---
title: "Motion"
subtitle: "Four easings, a duration scale, and the page tokens on top"
description: "The motion layer: four easing curves, a duration scale on the same t-shirt steps as spacing, and the semantic tokens that drive every page transition."
author: "Studio"
section: "Website"
subsection: "Foundations"
order: 8
status: "published"
---
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 |
| --- | --- | --- |
| `var(--ease-in)` | `cubic-bezier(0.4, 0, 1, 1)` | Exits. Slow start, accelerating away — the element leaves under its own steam |
| `var(--ease-out)` | `cubic-bezier(0.16, 1, 0.3, 1)` | Entrances. Fast start with a long tail, so the element arrives and settles |
| `var(--ease-in-out)` | `cubic-bezier(0.65, 0, 0.35, 1)` | Swaps and anything continuous. Symmetric, so it reads as one sweep |
| `var(--ease-emphasized)` | `cubic-bezier(0.86, 0, 0.07, 1)` | Large layout shifts that should feel decisive. Near-still at both ends, fast through the middle |
```css
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 |
| --- | --- | --- | --- | --- | --- |
| `var(--duration-2xs)` | 100ms | Hovers and presses | `var(--duration-3xl)` | 1800ms | Long entrances |
| `var(--duration-xs)` | 200ms | Toasts, focus, button state | `var(--duration-4xl)` | 2000ms | Orchestrated sequences |
| `var(--duration-s)` | 400ms | Dropdowns, expanding rows | `var(--duration-5xl)` | 2500ms | Cinematic sequences |
| `var(--duration-m)` | 600ms | Drawers, dialogs, page fades | `var(--duration-6xl)` | 3000ms | Ambient loops |
| `var(--duration-l)` | 800ms | Full-area entrances | `var(--duration-7xl)` | 4000ms | Background motion |
| `var(--duration-xl)` | 1200ms | Hero motion and page transitions | `var(--duration-8xl)` | 6000ms | The slowest step available |
| `var(--duration-2xl)` | 1500ms | Extended reveals | `var(--duration-stagger)` | 30ms | A per-item delay step, not a duration |
## 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 |
| --- | --- | --- |
| `--motion-page-open-duration` · `-easing` | `--duration-xl` · `--ease-in-out` | A page arriving |
| `--motion-page-close-duration` · `-easing` | `--duration-xl` · `--ease-in-out` | A page leaving |
| `--motion-page-swap-duration` · `-easing` | `--duration-xl` · `--ease-in-out` | One container replacing another |
| `--motion-page-fade-duration` · `-easing` | `--duration-m` · `--ease-in-out` | 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.
```css
@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-bezier` or 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.