Motion is two primitive scales with a thin semantic layer on top. Easings say how a movement is shaped, durations say how long it takes, and the --motion-page-* tokens combine the two for the transitions between pages. Nothing on the site should reach for a raw cubic-bezier or a millisecond value.
Easing
Four curves, each with a job. The difference between them is where the movement spends its time.
| Token | Curve | Use |
|---|---|---|
cubic-bezier(0.4, 0, 1, 1) |
Exits. Slow start, accelerating away — the element leaves under its own steam | |
cubic-bezier(0.16, 1, 0.3, 1) |
Entrances. Fast start with a long tail, so the element arrives and settles | |
cubic-bezier(0.65, 0, 0.35, 1) |
Swaps and anything continuous. Symmetric, so it reads as one sweep | |
cubic-bezier(0.86, 0, 0.07, 1) |
Large layout shifts that should feel decisive. Near-still at both ends, fast through the middle |
ease-in
ease-out
ease-in-out
emphasized
transition-timing-function: var(--ease-out);
Duration
The same t-shirt steps as spacing and type, so the vocabulary carries across.
| Token | Value | Use | Token | Value | Use |
|---|---|---|---|---|---|
| 100ms | Hovers and presses | 1800ms | Long entrances | ||
| 200ms | Toasts, focus, button state | 2000ms | Orchestrated sequences | ||
| 400ms | Dropdowns, expanding rows | 2500ms | Cinematic sequences | ||
| 600ms | Drawers, dialogs, page fades | 3000ms | Ambient loops | ||
| 800ms | Full-area entrances | 4000ms | Background motion | ||
| 1200ms | Hero motion and page transitions | 6000ms | The slowest step available | ||
| 1500ms | Extended reveals | 30ms | A per-item delay step, not a duration |
2xs · 100ms
xs · 200ms
s · 400ms
m · 600ms
l · 800ms
xl · 1200ms
Page transitions
The semantic layer is small on purpose: today its only consumer is the page transition between documents, which the docs kit drives.
| Token | Resolves to | Governs |
|---|---|---|
· -easing |
· | A page arriving |
· -easing |
· | A page leaving |
· -easing |
· | One container replacing another |
· -easing |
· | The cross-fade inside a swap |
Open, close and swap all resolve to --duration-xl, so a navigation takes about 1.2 seconds end to end; the fade is --duration-m, 600ms. All four easings are --ease-in-out. These are engine defaults, deliberately untouched. They are :root tokens, so assets/css/theme.css can re-point them directly if the tempo should be brisker — the kit's wiring is inherited unchanged.
Naming a new semantic token
Semantic motion tokens follow a three-slot grammar, so a new one has a name before it has a value.
| Slot | Values |
|---|---|
| Scope | page · surface · element · feedback — how much of the screen the motion covers |
| Event | open · close · swap · enter · exit · fade · hover · press |
| Property | duration or easing, always the last slot |
So --motion-surface-enter-duration is legible before anyone looks it up, and that is the point of the grammar.
Reduced motion
The engine ships no global reset. There is no * rule zeroing every duration — instead each component carries its own @media (prefers-reduced-motion: reduce) block (thirteen of them in design-system.css), and assets/docs-kit/barba-docs.js checks matchMedia before it animates a page transition. Anything new has to do the same; nothing will do it on your behalf.
@media (prefers-reduced-motion: reduce) {
.thing { animation: none; }
}
- Every animation must survive being switched off. A reader with reduced motion set should still reach the same end state; if turning the motion off loses information, the motion was carrying meaning it should not have been.
- Write the guard with the animation, not afterwards. There is no global net to fall back on, so an animation shipped without one simply ignores the preference.
The two demos on this page stop when reduced motion is set, which a page about motion is the last place to get wrong.
Rules
- Never write a raw
cubic-bezieror a millisecond value. Both scales exist so that motion across the site stays recognisably one system. - Match the curve to the direction. Entrances take
--ease-out, exits take--ease-in; swapping them makes an element look like it is fighting the interaction. - Reach for a semantic token before a primitive. If a new kind of motion needs one and none exists, the semantic layer is missing a token — the fix is upstream, not a local millisecond.