Motion tokens define timing and rhythm, not intent. They are reused for page transitions, component animations, and micro-interactions depending on context.
The system is built in two layers, the same model as color and spacing: primitives (raw easings and durations, named on the same t-shirt scale as --space-* and --font-*) → semantic tokens (named by intent, e.g. --motion-page-open). Always use semantic tokens in components — never hardcode cubic-bezier() or millisecond values inline.
The philosophy: durations follow the same t-shirt scale as spacing and type. Easings use the standard CSS keyword names (in, out, in-out) with refined cubic-bezier values. Zero new vocabulary to memorise — if you already know how --space-m works, you already know how --duration-m works.
Easing Primitives
Three primitives, named after the standard CSS easing keywords. The cubic-bezier values are our refined replacements for the browser defaults.
| Token | Value | Use for |
|---|---|---|
cubic-bezier(0.4, 0, 1, 1) |
Exits. Fast start, slow end. The element accelerates away. | |
cubic-bezier(0.16, 1, 0.3, 1) |
Entrances. Slow start, slow end with a long graceful tail. The element settles into place. | |
cubic-bezier(0.65, 0, 0.35, 1) |
Swaps and continuous motion. Symmetric. Reads as a single sweep. |
ease-in
ease-out
ease-in-out
transition-timing-function: var(--ease-out);
Duration Primitives
Six durations on the same t-shirt scale as --space-*, --font-*, and --radius-*. Extendable in either direction (2xs, 2xl, etc.) when a new use case demands it.
| Token | Value | Use for |
|---|---|---|
100ms |
Hovers, presses, tooltips appearing | |
200ms |
Small UI feedback (toasts, focus rings, button state) | |
400ms |
Element-level transitions (dropdowns, expanding rows, fade swaps) | |
600ms |
Surface-level transitions (drawers, modals, page swaps) | |
800ms |
Page-level openings (full-area entrances) | |
1200ms |
Hero motion, deliberate reveals, intro animations |
2xs · 100ms
xs · 200ms
s · 400ms
m · 600ms
l · 800ms
xl · 1200ms
transition-duration: var(--duration-m);
Semantic Motion Tokens
Named by intent. These compose primitives. Components and product code only ever read the semantic layer — never the primitives directly.
In v1 only page-level semantic tokens exist, because that's the first consumer (studio's Barba page transitions). Element-, surface-, and feedback-level tokens will be added when a real component needs them.
| Semantic token | Composes | Used for |
|---|---|---|
| + | A page rising into view (slide-up). 800ms with a long graceful tail. | |
| + | A page falling away (slide-down). 600ms — slightly faster than open, because the user has already decided to leave. | |
| + | Sibling-page navigation (the conveyor). Single continuous sweep. | |
| + | Crossfade fallback when no direction is known. |
Each semantic token is stored as two CSS variables — one for duration, one for easing — so consumers can plug them into either CSS shorthand (transition) or JS animation APIs (element.animate(keyframes, { duration, easing })) without parsing.
/* The four page tokens, as declared in design-system.css */
--motion-page-open-duration: var(--duration-l);
--motion-page-open-easing: var(--ease-out);
--motion-page-close-duration: var(--duration-m);
--motion-page-close-easing: var(--ease-in-out);
--motion-page-swap-duration: var(--duration-m);
--motion-page-swap-easing: var(--ease-in-out);
--motion-page-fade-duration: var(--duration-s);
--motion-page-fade-easing: var(--ease-in-out);
Naming Convention
When a new consumer needs a new semantic motion token, follow this pattern:
--motion-{scope}-{event}-{property}
| Slot | Allowed values |
|---|---|
scope |
page · surface · element · feedback |
event |
open · close · swap · enter · exit · fade · hover · press |
property |
duration · easing |
Examples (not yet defined — to be added when first needed):
--motion-surface-open-duration— drawers, modals, the contact overlay--motion-element-hover-duration— buttons, links, hover states--motion-feedback-toast-duration— toast notifications appearing
The semantic name should describe what kind of motion event this is, not which component triggers it. A token called --motion-button-hover would be wrong — many things hover, not just buttons. --motion-element-hover is right.
Usage in CSS
Reference the duration and easing variables separately, the same way transition-duration and transition-timing-function already split in standard CSS:
.button {
transition-property: background, color, border-color;
transition-duration: var(--duration-2xs);
transition-timing-function: var(--ease-out);
}
.dialog {
transition-property: opacity, transform;
transition-duration: var(--motion-page-fade-duration);
transition-timing-function: var(--motion-page-fade-easing);
}
For component code, prefer the semantic token (--motion-*) when one exists. Fall back to the primitives (--duration-*, --ease-*) for genuinely new motion events that don't have a semantic name yet — but consider whether a new semantic token should be added first.
Usage in JavaScript
Read the tokens once at module load via getComputedStyle, then use them with the Web Animations API:
function readToken(name) {
return getComputedStyle(document.documentElement)
.getPropertyValue(name).trim();
}
const MOTION = {
pageOpen: {
duration: parseInt(readToken("--motion-page-open-duration"), 10),
easing: readToken("--motion-page-open-easing"),
},
};
element.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: MOTION.pageOpen.duration, easing: MOTION.pageOpen.easing }
);
This is the pattern used by studio/assets/js/studio-barba.js for page transitions.
Reduced Motion
The system does not define a separate set of tokens for prefers-reduced-motion users. Instead, individual consumers should check the user preference at animation time and either skip the animation or use --duration-2xs (effectively instant):
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const duration = reducedMotion ? 0 : MOTION.pageOpen.duration;
This keeps the token surface small. If a future component needs more nuanced reduced-motion behaviour, add a --motion-{event}-reduced-duration variant alongside it.