Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 17.5Advanced ~23 min · shot 5 of 6

@property and Houdini

In one line

An unregistered custom property is only text, so the browser cannot animate it. Registering it with @property gives it a type, and then gradients, angles and counters animate. The CSS Paint API goes further but is not in every browser, so it must be an optional extra.

0/6 · 0%

Think of it like this

A recipe card that says "add some of the red stuff". You cannot halve a quantity that is only words. Write "2 teaspoons of paprika" and you can halve it smoothly. @property turns the words into a typed quantity.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

@property
A CSS rule that registers a custom property with a type, an inheritance rule and an initial value.
syntax
The descriptor that names the type a registered property accepts, such as <angle>.
initial-value
The value a registered property has when nothing sets it, or when the set value is invalid.
Houdini
The name for a group of browser APIs that let your code take part in styling and painting (registered properties, the Paint API).
paint worklet
A small script run by the browser to draw a CSS image, loaded with CSS.paintWorklet.addModule().
progressive enhancement
Build a page that works everywhere, then add the better version where it is supported.

Step by step

01Turn both swatches

In "Registered or not" press Turn both. The left conic gradient rotates over a second; the right one jumps to the end. The track list shows one transition for --a and none for --b.

02Read the console

The first two lines count the transitions. The next two set a bare 5: the registered angle reads back 0deg, the unregistered one 5. The last two show the two ways registerProperty throws.

03Hover or Tab to the border

In "A turning border and a counter" hover Reserve, or Tab to it. The ring turns twice and stops. Pause at 0.5 s and read the track: turn, 1500 ms, ×2, and --turn is 120deg (a third of 360). The track's cost badge says paint: a custom property that drives a gradient repaints on every frame, as the Chromium trace in this topic shows.

04Count with a typed integer

Press Count to 80 km. The number runs from 0 to 80, and the track shows --n with an amber paint mark.

05Open the Paint API example

The status line says which path ran. In the checker's browser: the fallback. Where the worklet loads, the dots come from paint(dots) instead of the gradient.

Try it yourself

  1. 1

    Unregister the angle

    In "Registered or not" delete the @property block. Press Turn both: the left swatch now flips too, and the first console line prints registered, after the change: (0 transition).

  2. 2

    Change the initial value

    Set initial-value: 90deg. The bare 5 now reads back as 90deg: the fallback is the initial value.

  3. 3

    Stop the border for reduced motion

    In "A turning border and a counter" switch on Reduced motion and hover the button: no track appears and --turn stays 0deg.

Code & diagrams

Registered or not

Two identical gradients, one with @property. Only the registered angle transitions. The console shows the type fallback and the JavaScript registration errors.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

registered, after the change: --a (1 transition)
not registered, after the change: 0 transitions, it flipped
registered --a: 5 reads back as 0deg
not registered --b: 5 reads back as 5
registering the same name twice: InvalidModificationError
no initialValue for <length>: SyntaxError
A turning border and a counter

The Reserve border turns twice on hover or keyboard focus and not at all with reduced motion. The km counter animates a typed integer. Both are paint work, not composite.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

--turn at rest: 0deg
--n set to 80 reads back as: 80
--n set to 7.5 reads back as: 0
animating --n makes 1 animation, keyframes on: --n
A Paint API fallback

This example sets no output: the checker's browser has no CSS.paintWorklet, so it always shows the fallback line. In a Chromium-based browser the same code can load the worklet, depending on where the page runs. Nothing here may break when the API is missing.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Leave out inherits

In "Registered or not" delete inherits: false; from the @property rule.

terminal
$ Run and read the first lines
── what you'll see ──
registered, after the change: (0 transition)
registered --a: 5 reads back as 5

Myth vs fact

Myth

Houdini means every browser can run my paint worklet.

Fact

@property is Baseline since July 2024. The Paint API is experimental and not Baseline. Treat it as an extra with a fallback.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A registered property computes to a real value, so calc() and var() on it work in typed ways. Inheritance has a cost: changing an inheriting property restyles all the descendants, so keep inherits: false unless children need the value.

  • ▸

    If the same name is registered with @property and with CSS.registerProperty(), the JavaScript registration wins (MDN).

  • ▸

    A paint worklet's result is still painted: it is not composite work. Animating its inputs repaints it each frame.

Remember this

  1. 1

    The problem this solves: transition: --a 1s on a custom property does nothing, because the browser sees 0deg and 180deg as two pieces of text and cannot work out the values between them. Gradients had the same trouble: you could not animate a gradient's angle, so people swapped whole backgrounds.

  2. 2

    Registering. @property --a { syntax: "<angle>"; inherits: false; initial-value: 0deg; } tells the browser the type. syntax is the type (<angle>, <length>, <color>, <integer>, <number>, <percentage> or *). inherits says whether children get the value. initial-value is the starting and fallback value. syntax and inherits are required, and initial-value is required unless the syntax is *; a rule that misses one is ignored whole (MDN). It is Baseline 2024, newly available since July 2024.

  3. 3

    Measured. In "Registered or not" the registered --a made one transition, --a, and the unregistered --b made none: it flipped at once. A value of the wrong type falls back: 5 set on the registered angle reads back as 0deg, and on the unregistered one stays 5. CSS.registerProperty() does the same from JavaScript; registering the same name twice throws InvalidModificationError, and a <length> with no initialValue throws SyntaxError.

  4. 4

    What it enables. A gradient border that turns: put a conic-gradient(from var(--turn), …) in the border-box layer and animate --turn from 0 to 360deg. A counter: animate an <integer> and print it with counter(). Setting an integer property to 7.5 read back as 0. The cost: we traced Chromium 153 for one second. The turning gradient ran style, paint and commit on all 60 frames and no layout. The counter ran layout on all 60 frames because its text changes. A transform rotation ran none of these. So @property animations are paint work; use them for a small button, not a full-screen background.

  5. 5

    The Paint API. CSS.paintWorklet.addModule(url) loads a script that calls registerPaint("dots", class { paint(ctx, size, props) { … } }), and CSS then says background: paint(dots). You draw with a canvas-like context. MDN lists it as experimental and not Baseline, available only in secure contexts, and its table shows limited support, so check it before you use it. Always feature-detect with "paintWorklet" in CSS, give a plain CSS fallback first, and let nothing depend on it. The checker's browser has no CSS.paintWorklet, so "A Paint API fallback" always runs its fallback path.

  6. 6

    Accessibility. A moving border is decoration: stop it for reduced motion (the build points --turn-name at none), run it for a limited time (two turns, not forever), and give keyboard focus the same effect as hover.

Explain it without notes

01

Why can't the browser animate an unregistered custom property?

02

Which descriptors does @property require?

03

How should a page use the CSS Paint API?

Practice

01

Register a <color> property and animate a button's glow colour from orange to blue on hover.

02

Write a @supports guard so a browser without gradient support gets a plain border.

Done when you can

  • You can register a property with all its descriptors and say what each does

  • You can animate an angle, a colour and an integer, and say what that costs

  • You can explain what an unregistered property does

  • You can use the Paint API only as a feature-detected extra