Motion Design Tokens
Systematize animation across every surface by encoding easing, duration, and delay decisions into portable, platform-agnostic motion tokens.
6 min read
The full lesson
Hardcoded animation values are the leading cause of incoherent motion across products. One component uses 300ms ease. Another uses a cubic-bezier copied from Stack Overflow. The values are slightly different everywhere, and the result is a UI that feels subtly unfinished — even when every individual screen looks polished.
Motion design tokens fix this. They make easing, duration, and delay first-class named design decisions. You store those decisions once and use them everywhere: CSS, JavaScript, iOS, Android, and documentation.
This lesson covers how to structure motion tokens from primitives to components, write them in the W3C DTCG format, consume them in CSS and JavaScript, connect them to Figma, and make them accessible by default.
Why Motion Tokens Belong in Your Design System
Color and typography tokens are now standard in any mature design system. Motion tokens are the natural next step — and for the same reasons. Without them:
- A component built by one engineer uses different timing than an identical component built by another.
- Platform teams (iOS, web, Android) each maintain separate hardcoded motion constants with no shared source of truth.
- Updating the system-wide “standard” duration means a grep-and-replace across hundreds of files, and values inevitably drift.
- Accessibility compliance — respecting
prefers-reduced-motion— has to be re-implemented independently in every component.
With motion tokens, those decisions are centralized. Change --motion-duration-standard from 300ms to 250ms and every consuming component picks up the update automatically.
The Three-Tier Token Architecture
Motion tokens follow the same three-tier structure as modern color and spacing tokens: primitive → semantic → component.
Primitive (Reference) Tokens
Primitives are raw, context-free values. They define the full set of motion values your system supports — nothing more.
{
"motion": {
"duration": {
"0": { "$value": "0ms", "$type": "duration" },
"50": { "$value": "50ms", "$type": "duration" },
"100": { "$value": "100ms", "$type": "duration" },
"200": { "$value": "200ms", "$type": "duration" },
"300": { "$value": "300ms", "$type": "duration" },
"500": { "$value": "500ms", "$type": "duration" },
"700": { "$value": "700ms", "$type": "duration" }
},
"easing": {
"linear": { "$value": "linear", "$type": "cubicBezier" },
"ease-in": { "$value": "cubic-bezier(0.4, 0, 1, 1)", "$type": "cubicBezier" },
"ease-out": { "$value": "cubic-bezier(0, 0, 0.3, 1)", "$type": "cubicBezier" },
"ease-in-out": { "$value": "cubic-bezier(0.4, 0, 0.2, 1)", "$type": "cubicBezier" },
"spring-gentle":{ "$value": "linear(0,0.009,0.035 2.1%,0.141,0.281 6.7%,0.723 12.9%,0.938 16.7%,1.022,1.03,1.022,1 25%,0.984,0.985 30.2%,1)", "$type": "cubicBezier" }
}
}
}
The $value / $type syntax is the W3C Design Token Community Group (DTCG) stable format. Use it from the start. Tool support is now broad — Style Dictionary v4, Token Transformer, and Theo all support it. It also future-proofs your system against proprietary formats that lock you into a single platform.
Semantic (System) Tokens
Semantic tokens give purpose to primitives. They answer “what is this token for?” — not “what value does it hold?” Engineers should always work at this tier. They should never reach directly for a primitive.
{
"motion": {
"enter": {
"duration": { "$value": "{motion.duration.200}", "$type": "duration" },
"easing": { "$value": "{motion.easing.ease-out}", "$type": "cubicBezier" }
},
"exit": {
"duration": { "$value": "{motion.duration.100}", "$type": "duration" },
"easing": { "$value": "{motion.easing.ease-in}", "$type": "cubicBezier" }
},
"standard": {
"duration": { "$value": "{motion.duration.300}", "$type": "duration" },
"easing": { "$value": "{motion.easing.ease-in-out}", "$type": "cubicBezier" }
},
"expressive": {
"duration": { "$value": "{motion.duration.500}", "$type": "duration" },
"easing": { "$value": "{motion.easing.spring-gentle}", "$type": "cubicBezier" }
},
"instant": {
"duration": { "$value": "{motion.duration.0}", "$type": "duration" },
"easing": { "$value": "{motion.easing.linear}", "$type": "cubicBezier" }
}
}
}
The alias syntax — for example, {motion.duration.200} — keeps the relationship between tiers explicit. Tools can also validate that referenced tokens actually exist.
Component Tokens
Component tokens are optional, scoped overrides. Use them when a specific component needs to deviate from the semantic defaults. A bottom sheet has different timing needs than a tooltip. Component tokens let you handle that without introducing unnamed magic values.
{
"bottom-sheet": {
"motion": {
"open-duration": { "$value": "{motion.enter.duration}", "$type": "duration" },
"open-easing": { "$value": "{motion.expressive.easing}", "$type": "cubicBezier" },
"close-duration": { "$value": "{motion.exit.duration}", "$type": "duration" },
"close-easing": { "$value": "{motion.exit.easing}", "$type": "cubicBezier" }
}
}
}
The bottom sheet gets its own tunable values, but they stay grounded in the semantic tier.
Consuming Motion Tokens in CSS
Style Dictionary (or an equivalent build step) transforms the DTCG JSON into platform outputs. For CSS, that means custom properties:
:root {
/* Durations */
--motion-duration-instant: 0ms;
--motion-duration-fast: 100ms;
--motion-duration-standard: 200ms;
--motion-duration-slow: 300ms;
--motion-duration-deliberate: 500ms;
/* Easings */
--motion-easing-enter: cubic-bezier(0, 0, 0.3, 1);
--motion-easing-exit: cubic-bezier(0.4, 0, 1, 1);
--motion-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
--motion-easing-expressive: linear(0, 0.009, 0.035 2.1%, ...);
}
Using a token in a component is then simple and readable:
.dialog {
transition:
opacity var(--motion-duration-standard) var(--motion-easing-enter),
transform var(--motion-duration-standard) var(--motion-easing-enter);
}
.dialog[data-state="closing"] {
transition:
opacity var(--motion-duration-fast) var(--motion-easing-exit),
transform var(--motion-duration-fast) var(--motion-easing-exit);
}
An engineer reading this code immediately understands: “standard duration, enter easing.” They don’t need to know the actual millisecond value.
Consuming Motion Tokens in JavaScript
For animation libraries like Framer Motion, Motion One, or GSAP, you can read CSS custom properties at runtime:
const style = getComputedStyle(document.documentElement);
const durationStandard = style.getPropertyValue('--motion-duration-standard').trim();
// "300ms"
// Strip 'ms' and parse to a number for library use
const ms = parseFloat(durationStandard);
motion(element, { opacity: [0, 1] }, { duration: ms / 1000, easing: 'ease-out' });
Alternatively, export tokens as a JavaScript object from your build pipeline:
// tokens.js (generated by Style Dictionary)
export const motionDurationStandard = 300;
export const motionEasingEnter = 'cubic-bezier(0, 0, 0.3, 1)';
Both approaches keep JavaScript and CSS in sync. They both read from the same source JSON, so a change to the JSON propagates to both outputs.
Accessible Defaults: Baking in Reduced Motion
The most common mistake with motion tokens is treating prefers-reduced-motion as an afterthought — something layered on after the “real” animation is done. A better approach: build reduced-motion safety into the token system itself.
One pattern is to redefine your semantic tokens inside the prefers-reduced-motion: reduce media query:
:root {
--motion-duration-standard: 300ms;
--motion-easing-enter: cubic-bezier(0, 0, 0.3, 1);
}
@media (prefers-reduced-motion: reduce) {
:root {
--motion-duration-standard: 0ms;
--motion-easing-enter: linear;
}
}
Any component using --motion-duration-standard now automatically respects the user’s motion preference. No component-level media query needed. The accessibility behavior lives in the token layer, not scattered across hundreds of components.
A more nuanced approach uses instant (zero duration) for large positional motion and fast (100ms fade) for opacity changes. This keeps state transitions perceivable even in reduced mode:
@media (prefers-reduced-motion: reduce) {
:root {
--motion-duration-enter: 0ms;
--motion-duration-standard: 0ms;
--motion-duration-exit: 0ms;
/* Fade remains: small, non-vestibular motion */
--motion-duration-fade: 150ms;
}
}
Do
Encode prefers-reduced-motion overrides at the CSS custom property level so every consuming component inherits safe defaults for free. Define a --motion-duration-fade token that stays non-zero in reduced mode, so state changes remain perceivable without triggering vestibular effects.
Don't
Write a @media (prefers-reduced-motion: reduce) block inside every individual component. This guarantees drift — some components will be missed, others overridden, and compliance becomes impossible to audit. Also, do not zero out opacity transitions in reduced motion mode; fades are generally safe and still communicate state change.
Figma Integration
Motion tokens live in code, but designers make motion decisions in Figma. Keeping the two in sync requires intentional tooling:
- Variables (Spring 2024+): Figma now supports duration and easing as variable types. Define your semantic motion tokens as Figma variables and connect them to prototype transition settings. Designers work with named tokens like “enter / standard” — not raw millisecond values.
- Token plugins: Tokens Studio (formerly Figma Tokens) can sync a DTCG-formatted JSON file with Figma. It pushes token changes to design and pulls design changes back to code, creating a genuine single source of truth instead of two separate systems that “try to match.”
- Documentation: Give each semantic token a
"$description"in the JSON that explains when to use it. Tools like Supernova and zeroheight can ingest those descriptions and render them as living documentation — designers and engineers read the same usage guidance.
The anti-pattern to avoid: a Figma file where motion values are manually typed as prototype transition numbers with no mapping to code tokens. Those numbers drift within weeks.
Motion Token Taxonomy: What to Include
A complete motion token vocabulary covers five categories:
| Category | Examples | CSS property |
|---|---|---|
| Duration | instant, fast, standard, slow, deliberate | transition-duration, animation-duration |
| Easing | enter, exit, standard, expressive, linear | transition-timing-function, animation-timing-function |
| Delay | none, short (50ms), medium (100ms) | transition-delay, animation-delay |
| Iteration | once, twice, infinite | animation-iteration-count |
| Fill mode | forward, backward, both | animation-fill-mode |
Duration and easing are the highest-leverage tokens — they govern 90% of motion decisions. Delay tokens prevent hardcoded stagger values from proliferating. Iteration and fill-mode tokens are rare but useful for emphasis animations and looping states like loading spinners.
What Motion Tokens Are Not
Motion tokens define reusable values. They do not define complete animation specifications. They do not encode:
- Which properties to animate — that is a component-level decision. A token says “use this duration and curve,” not “animate transform and opacity.”
- Choreography or sequencing — the order in which elements enter the screen is controlled by stagger logic and orchestration patterns, not tokens.
- Spring physics parameters — stiffness, damping, and mass cannot be directly represented as CSS custom properties. The workaround is to pre-compute a
linear()approximation of the spring and store that as a token value, as shown in the primitive example above.
Auditing Existing Motion for Token Opportunities
If you are introducing motion tokens into a codebase that does not have them, start with an audit:
- Grep for hardcoded values: Search for
transition:,animation:,duration, andeasein CSS and JS. Count the unique duration values and unique easing curves. - Cluster by role: Group similar values.
200ms,210ms, and190mslikely share the same semantic intent — “fast.” - Name by purpose, not value: Do not name a token
duration-200. Name itduration-fast. If you change the value to180msin a future update, the name stays accurate. - Replace and measure: Introduce the token layer, replace hardcoded values, and run a visual regression test to confirm animation behavior is preserved.