EmpireUI
Get Pro
← Blog7 min read#motion-design#animation-tokens#design-systems

Motion Design System: Timing, Easing, and Token Standards

Stop hardcoding 300ms everywhere. Learn how to build a motion design system with timing tokens, easing curves, and shared standards your whole team can use.

Abstract flowing light trails on a dark background representing motion and animation timing

Why Motion Tokens Matter More Than You Think

Honestly, most codebases treat animation like an afterthought — a transition: all 0.3s ease slapped on a button hover state and called a day. That works until you have six developers, three component libraries, and nobody agreeing on what "fast" means.

A motion design system is just a set of shared rules. Timing values, easing functions, and duration scales — all codified as tokens the same way you'd codify colors or spacing. The goal isn't visual polish for its own sake. It's consistency and predictability across every interactive surface in your product.

When you pair motion tokens with something like a spacing system, your UI starts to feel intentional rather than assembled. Animations that reference the same scale as your layout grid create a coherent rhythm. That coherence is what users notice — even if they can't articulate why.

The Duration Scale: Building Your Timing Vocabulary

Duration tokens work exactly like a type scale. You define named stops — duration-fast, duration-base, duration-slow, duration-deliberate — and everything in your codebase picks from that list. No more magic numbers.

A reasonable starting scale for a component library looks like this: 100ms for micro-interactions (checkbox ticks, icon swaps), 200ms for state transitions (hover, focus rings), 300ms for element entrances and exits, and 500ms or higher for page-level transitions or modal overlays. These aren't arbitrary. Human perception of motion flickers out below about 80ms and starts feeling sluggish above 600ms for most UI interactions.

In Tailwind v4.0.2 you can register these as CSS custom properties and expose them via the theme. In CSS-first mode, that means dropping them in @theme so they cascade everywhere without JavaScript overhead. The trick is keeping the token names semantic rather than literal — --duration-fast: 100ms not --duration-100ms: 100ms, because the value might change and the name should still make sense.

Easing Curves: The Part Everyone Gets Wrong

Duration is only half the story. An animation that uses linear easing for 200ms feels completely different from one using cubic-bezier(0.4, 0, 0.2, 1) for the same duration. Most developers reach for ease, ease-in, or ease-out from the CSS shorthand list and stop there. That's leaving a lot of expressiveness on the table.

The four easing tokens you actually need: a standard curve for most UI transitions (cubic-bezier(0.4, 0, 0.2, 1) — Material Design's default), an enter curve for elements appearing (cubic-bezier(0, 0, 0.2, 1), decelerates into place), an exit curve for elements leaving (cubic-bezier(0.4, 0, 1, 1), accelerates out), and a spring-approximation for expressive moments (cubic-bezier(0.34, 1.56, 0.64, 1) — slight overshoot).

What's the difference between enter and exit curves? It's all about where the motion feels natural. Objects entering a screen should decelerate as they arrive — they're landing. Objects leaving should accelerate away — they're departing. Using the same easing in both directions is the most common motion mistake in production UIs.

Defining Tokens in CSS and JavaScript

The implementation looks different depending on your stack. For pure CSS projects, custom properties in :root do the job cleanly. For React component libraries (like Empire UI), you want the tokens available both in CSS for Tailwind utility classes and in JavaScript for libraries like Framer Motion or React Spring.

Here's a token file that covers both surfaces without duplication:

// tokens/motion.ts
export const motionTokens = {
  duration: {
    fast: 100,       // ms
    base: 200,
    slow: 300,
    deliberate: 500,
  },
  easing: {
    standard:  'cubic-bezier(0.4, 0, 0.2, 1)',
    enter:     'cubic-bezier(0, 0, 0.2, 1)',
    exit:      'cubic-bezier(0.4, 0, 1, 1)',
    expressive:'cubic-bezier(0.34, 1.56, 0.64, 1)',
  },
} as const;

// Inject as CSS custom properties at app root
export function injectMotionTokens() {
  const root = document.documentElement;
  root.style.setProperty('--duration-fast',      `${motionTokens.duration.fast}ms`);
  root.style.setProperty('--duration-base',      `${motionTokens.duration.base}ms`);
  root.style.setProperty('--duration-slow',      `${motionTokens.duration.slow}ms`);
  root.style.setProperty('--duration-deliberate',`${motionTokens.duration.deliberate}ms`);
  root.style.setProperty('--ease-standard',  motionTokens.easing.standard);
  root.style.setProperty('--ease-enter',     motionTokens.easing.enter);
  root.style.setProperty('--ease-exit',      motionTokens.easing.exit);
  root.style.setProperty('--ease-expressive',motionTokens.easing.expressive);
}

Calling injectMotionTokens() once in your app root means every component can reference var(--duration-base) in CSS while animation libraries read directly from the TypeScript object. Single source of truth. No sync problems.

Reduced Motion: Accessibility Is Not Optional

The prefers-reduced-motion media query exists because some users experience nausea, dizziness, or seizures from animated interfaces. Ignoring it isn't a design choice — it's an accessibility failure. The WCAG accessibility guide covers this under WCAG 2.1 criterion 2.3.3.

The cleanest approach at the token level is to override all duration tokens to 0ms (or a near-zero value like 1ms if you need transitions to still fire for JavaScript callbacks) when prefers-reduced-motion: reduce is active. One block of CSS handles every component simultaneously:

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast:       1ms;
    --duration-base:       1ms;
    --duration-slow:       1ms;
    --duration-deliberate: 1ms;
  }
}

This is far better than hunting through individual component stylesheets to add motion overrides. Token-level reduced motion means you implement the accessibility rule exactly once. New components automatically inherit it.

Integrating Motion Tokens with Tailwind v4

Tailwind v4's CSS-first configuration makes registering motion tokens genuinely ergonomic. You define them inside @theme and Tailwind generates utility classes automatically. No plugin needed, no tailwind.config.js dancing.

/* app.css */
@import "tailwindcss";

@theme {
  --duration-fast:       100ms;
  --duration-base:       200ms;
  --duration-slow:       300ms;
  --duration-deliberate: 500ms;

  --ease-standard:   cubic-bezier(0.4, 0, 0.2, 1);
  --ease-enter:      cubic-bezier(0, 0, 0.2, 1);
  --ease-exit:       cubic-bezier(0.4, 0, 1, 1);
  --ease-expressive: cubic-bezier(0.34, 1.56, 0.64, 1);
}

@media (prefers-reduced-motion: reduce) {
  @theme {
    --duration-fast:       1ms;
    --duration-base:       1ms;
    --duration-slow:       1ms;
    --duration-deliberate: 1ms;
  }
}

With this setup, Tailwind generates classes like duration-base, duration-slow, and ease-standard that map directly to your tokens. Components built with Empire UI's 40 visual styles all reference the same motion vocabulary. Swapping a theme — say from glassmorphism to neobrutalism — can redefine the easing tokens without touching a single component file. That's the token approach paying off.

Choreography: Staggering and Sequencing at Scale

Individual component transitions are the easy part. Where motion systems get interesting — and where most teams give up — is coordinated animation across multiple elements. A list that animates in item-by-item. A dashboard where cards cascade onto the screen. Modals where the overlay fades while the panel slides in simultaneously.

The choreography layer sits on top of your tokens. You define relationships: what animates first, what follows, and by how much delay. A standard stagger delay of 40ms between list items feels natural without being distractingly slow. That 40ms isn't magic — it's derived from the duration-fast token divided by 2.5, keeping the choreography proportional to the rest of the system.

Tools like Framer Motion make this approachable with staggerChildren on a parent motion.div. But even without a dedicated library, you can implement stagger with CSS custom property tricks — setting animation-delay: calc(var(--index) * 40ms) on each child via inline styles. Keep the stagger multiplier as its own token (--stagger-delay: 40ms) so it can be tuned globally. Check out how the icon system handles animated icon states for a real-world example of small-scale choreography done right.

Documenting Motion in Your Design System

Tokens without documentation are just variables. Your team needs to understand what each token is for, when to reach for duration-slow versus duration-deliberate, and which easing curve fits which interaction type. This is where Storybook earns its keep — a Storybook component library setup lets you build living motion examples that show the actual animation, not just a static spec.

A good motion doc page shows side-by-side comparisons: the same element animated with ease-enter vs ease-standard vs ease-expressive. Developers can see the difference immediately. Written descriptions of easing curves are almost useless — "decelerates smoothly" means nothing until you see it.

Consider adding a motion audit checklist to your PR template: Does this component respect prefers-reduced-motion? Does it use a duration token or a hardcoded millisecond value? Is the easing curve appropriate for the interaction type (enter/exit/state-change)? Three questions. Takes 30 seconds. Catches 90% of motion system violations before they hit production.

FAQ

What's the difference between duration tokens and delay tokens in a motion system?

Duration tokens define how long an individual animation takes to complete. Delay tokens (or stagger values) define when an animation starts relative to a trigger or a sibling element's animation. They serve different roles — duration affects perceived speed, delay controls sequencing and choreography. Keep them as separate token categories.

Should I use CSS transitions or CSS animations for token-based motion?

CSS transitions work well for state changes driven by class or property changes — hover states, focus rings, toggled visibility. CSS animations (with @keyframes) are better for multi-step sequences or looping effects that aren't tied to a single state flip. Both can reference the same custom property tokens for duration and easing, so the choice is about control flow, not token compatibility.

How do I handle motion tokens in a multi-theme design system where themes have different personalities?

Define the token names once at the system level and let each theme override the values. A "playful" theme might set --ease-standard to cubic-bezier(0.34, 1.56, 0.64, 1) (springy overshoot) while an "enterprise" theme keeps it at cubic-bezier(0.4, 0, 0.2, 1) (crisp and neutral). The component code never changes — only the token values per theme. This is the same pattern used for color tokens in a color system.

Framer Motion uses seconds, CSS uses milliseconds — how do I keep tokens consistent?

Store your canonical values in milliseconds as integers in a TypeScript token object (e.g., duration.base = 200). When passing to Framer Motion, divide by 1000: duration: motionTokens.duration.base / 1000. When injecting to CSS custom properties, append 'ms'. The conversion is mechanical and one-directional — your token file stays in ms, consumer code handles the unit.

Is 300ms actually a good default for most UI transitions?

For entering and exiting elements (modals, dropdowns, toasts) 250–350ms is the established sweet spot backed by UX research — fast enough not to block interaction, slow enough to feel polished rather than janky. For hover and focus state transitions, 150–200ms is better. The mistake is using 300ms for everything. Match duration to the spatial scale of the animation: small movements need shorter durations.

Can I use CSS `transition: all` with a motion token duration?

Technically yes, but transition: all is a performance trap. It transitions every animatable CSS property including ones you didn't intend — color, opacity, transforms, borders all at once. This triggers layout recalculations and can cause jank. Always specify the properties explicitly: transition: transform var(--duration-base) var(--ease-standard), opacity var(--duration-base) var(--ease-standard). More verbose, but GPU-compositable and predictable.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Animation in Design Systems: Tokens, Curves, Duration ScaleTesting a Design System: Visual, Unit, and Accessibility TestsAnimation Design in 2027: AI-Generated Motion, CSS NativeCSS Animations & Motion Design: The Complete 2026 Playbook