Shots in this scene
@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.
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
Unregister the angle
In "Registered or not" delete the
@propertyblock. Press Turn both: the left swatch now flips too, and the first console line printsregistered, after the change: (0 transition). - 2
Change the initial value
Set
initial-value: 90deg. The bare5now reads back as90deg: the fallback is the initial value. - 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
--turnstays0deg.
Code & diagrams
Two identical gradients, one with @property. Only the registered angle transitions. The console shows the type fallback and the JavaScript registration errors.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
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>: SyntaxErrorThe 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.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
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: --nThis 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.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
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.
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()andvar()on it work in typed ways. Inheritance has a cost: changing an inheriting property restyles all the descendants, so keepinherits: falseunless children need the value. - ▸
If the same name is registered with
@propertyand withCSS.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
The problem this solves:
transition: --a 1son a custom property does nothing, because the browser sees0degand180degas 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
Registering.
@property --a { syntax: "<angle>"; inherits: false; initial-value: 0deg; }tells the browser the type.syntaxis the type (<angle>,<length>,<color>,<integer>,<number>,<percentage>or*).inheritssays whether children get the value.initial-valueis the starting and fallback value.syntaxandinheritsare required, andinitial-valueis required unless the syntax is*; a rule that misses one is ignored whole (MDN). It is Baseline 2024, newly available since July 2024. - 3
Measured. In "Registered or not" the registered
--amade one transition,--a, and the unregistered--bmade none: it flipped at once. A value of the wrong type falls back:5set on the registered angle reads back as0deg, and on the unregistered one stays5.CSS.registerProperty()does the same from JavaScript; registering the same name twice throwsInvalidModificationError, and a<length>with noinitialValuethrowsSyntaxError. - 4
What it enables. A gradient border that turns: put a
conic-gradient(from var(--turn), …)in theborder-boxlayer and animate--turnfrom 0 to 360deg. A counter: animate an<integer>and print it withcounter(). Setting an integer property to7.5read back as0. 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. Atransformrotation ran none of these. So@propertyanimations are paint work; use them for a small button, not a full-screen background. - 5
The Paint API.
CSS.paintWorklet.addModule(url)loads a script that callsregisterPaint("dots", class { paint(ctx, size, props) { … } }), and CSS then saysbackground: 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 noCSS.paintWorklet, so "A Paint API fallback" always runs its fallback path. - 6
Accessibility. A moving border is decoration: stop it for reduced motion (the build points
--turn-nameatnone), run it for a limited time (two turns, not forever), and give keyboard focus the same effect as hover.
Explain it without notes
Why can't the browser animate an unregistered custom property?
Which descriptors does @property require?
How should a page use the CSS Paint API?
Practice
Register a <color> property and animate a button's glow colour from orange to blue on hover.
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