Why it works, the rules and the credits1 reason · 3 rules · support · credits
Why it works
Motion exists only inside the query, so the default is none. People who never touched the setting match no-preference and see it.
Rules
Build the state change first and add the motion inside the query. Do not strip motion afterwards with a global rule that sets every duration to 0.01ms.
Fades of opacity and color are not motion and can stay outside the query.
Animation driven by a script checks the same query with matchMedia.
Toggle the dropdown. It fades over 180ms on --ease-out. Then run the curves. The --ease-out square leaves faster than plain ease-out and settles for longer. The --ease-in-out square waits longer than plain ease-in-out, then crosses the middle faster.
Why it works, the rules and the credits3 reasons · 5 rules · support · credits
Why it works
An ease-out curve starts fast, so the interface moves at the moment the user is watching. Use --ease-out for anything entering or leaving.
An ease-in-out curve suits something that is already on screen and moves to a new place. Use --ease-in-out there.
The built-in ease-out and ease-in-out keywords are weak versions of the same shapes. These two are stronger.
Rules
Never use ease-in on UI. It starts slow and reads as lag.
Keep a UI transition at 300ms or less. Press feedback takes 100 to 160ms, tooltips and small popovers 125 to 200ms, dropdowns 150 to 250ms. The one exception is a modal or drawer, which takes 200 to 500ms.
Name the properties in a transition. Never write transition: all.
Other entries write var(--ease-out, ease-out), so their snippets work before these tokens exist.
Whether something should animate at all is not a CSS question. Emil Kowalski's animate and review-animations skills answer it.
Use it when one state change should drive several values together, or when a script feeds a number such as pointer position or progress into CSS and the result should ease.
Hover the card, or tab to its link. The arrow turns, the bar grows and the last line fades in, all together, and the readout runs through the numbers between 0 and 1 instead of jumping.
Why it works, the rules and the credits3 reasons · 5 rules · support · credits
Why it works
An unregistered custom property is a string, and a string cannot be interpolated. Registering it with a syntax gives it a type, and the browser then transitions it like any number.
One transition with one easing drives every dependent value. The children derive theirs with calc(), so the parts stay in step.
inherits: true passes the number down to the children that use it.
Rules
initial-value is required for every syntax except "*".
rotate, scale and translate do nothing on an inline element. Give the element display: inline-block or another box type first.
A registration is global. Register each name once and keep the names specific.
A script only writes the variable, as in el.style.setProperty("--x", "40%"). The easing and duration stay in CSS.
Other useful types are <length-percentage> for a gradient position and <angle> for a conic gradient.
Support
Chrome
85
Firefox
128
Safari
16.4
Credits
Borrowed from: Stripe, on the stripe.com home page and its sales contact page
Why it works, the rules and the credits3 reasons · 3 rules · support · credits
Why it works
A box-shadow transition repaints the shadow on every frame. Each shadow here is painted once, and the transition changes only opacity, which the compositor handles. In a Chrome trace of one card it took 12 paint events, against about 78 for a plain box-shadow transition.
The two shadows cross-fade, so the end state is exactly the raised shadow and not the sum of both.
isolation: isolate gives the card its own stacking context. z-index: -1 then puts the pseudo-elements above the card's background and below its content.
Rules
The card needs its own background. The pseudo-elements have none.
Follow the link. The two pages crossfade, the header holds still, and the readout says yes. In Firefox the page changes at once.
CodeCSS · 1 line
1
@view-transition{navigation: auto;}
Why it works, the rules and the credits1 reason · 2 rules · support · credits
Why it works
On a same-origin navigation the browser snapshots the old and new page and crossfades them. Parts that do not change, such as the header, appear to stay in place.
Rules
Both pages must include the rule.
The default crossfade adds no movement and needs no reduced-motion guard. If you add slides, put the rule inside @media (prefers-reduced-motion: no-preference).
Use it for the underline on a tab row or the bar beside a side nav. It replaces a script that measures the active item and writes a transform and a width.
Click the links in each row. The bar slides under that row's current link and takes its width. The bar in the other row does not move, because each row scopes its own anchor.
CodeHTML · CSS · 2 blocks · 34 lines
1
2
3
4
5
6
7
<navclass="tabs"><ul><li><ahref="/overview"aria-current="page">Overview</a></li><li><ahref="/pricing">Pricing and billing</a></li><li><ahref="/docs">Docs</a></li></ul></nav>
Why it works, the rules and the credits3 reasons · 4 rules · support · credits
Why it works
The anchor name sits on whichever item carries aria-current. Move the attribute and the anchor moves with it. The same attribute tells a screen reader which item is current, so there is no separate "active" class to keep in sync.
anchor() and anchor-size() resolve to the item's edge and size. A longer label, a larger font or a wrapped line needs no new measurement.
The bar is one element whose inset and size change, so a plain transition slides it.
Rules
Keep anchor-scope on the component root. Without it an anchor name is global, and two navs on one page both follow the last active item.
The list needs position: relative, so the bar is positioned against the box that holds the items.
For a vertical list swap the axes. Use inset-block-start: anchor(top) and block-size: anchor-size(height), and transition those two.
Give the active item a second cue such as weight or color. A browser without anchor positioning drops the anchor() declarations and the bar has no width.
Support
Chrome
131
Firefox
147
Safari
26
Firefox places the bar and does not slide it.
Credits
Borrowed from: Stripe, which ships the vertical form for a side nav in its home page stylesheet. The research found the rules and no page that renders the component, so this is from the stylesheet alone.