Motion tokens in Tailwind. Choreographed.
Build a motion token system in Tailwind 4 with consistent duration and easing, then respect prefers-reduced-motion automatically.
Four durations, three curves.
A 2026 motion token system has four duration steps: quick 100ms (toggles, focus rings), snappy 200ms (hovers, micro-feedback), smooth 350ms (modals, drawers, tooltips), deliberate 600ms (page transitions, hero reveals). Anything beyond 600ms feels broken on the web. Three easing curves cover most cases: cubic-bezier(0.2, 0.7, 0.2, 1) as the universal feels-right curve, ease-out for elements entering, ease-in for elements exiting. Encode the scale as Tailwind theme.extend.transitionDuration and transitionTimingFunction keys; reference them as utility classes (duration-snappy ease-standard) across components. Respect prefers-reduced-motion by dropping durations to about 0.01s inside @media (prefers-reduced-motion: reduce); Tailwind's motion-safe: and motion-reduce: variants handle this declaratively. Required for WCAG 2.2 SC 2.3.3. Brand book defines motion personality in a few sentences; design system encodes the values; components reference tokens, never raw ms or cubic-bezier strings.
A system, not a guess.
Without tokens, every component picks its own animation duration. The dropdown takes 150ms; the modal takes 300ms; the page transition takes 400ms; the toast notification takes 250ms. None of these were decided systematically; each was chosen at the moment by whoever built the component. The aggregate effect is a product that feels uneven, with motion behavior that varies arbitrarily between surfaces. Users do not consciously notice, but the product reads as less polished.
With tokens, motion is a named system. Quick 100ms applies to toggles, switches, focus rings, and other immediate feedback. Snappy 200ms applies to hovers, button presses, small UI confirmations. Smooth 350ms applies to modals appearing, drawers sliding, tooltips fading in. Deliberate 600ms applies to page transitions, hero element reveals, large surface changes. Every component references one of the four; no component invents its own number.
The cognitive load drops for everyone. Designers do not debate whether the hover should be 180ms or 220ms; they pick snappy. Engineers do not ask the designer; they use duration-snappy. Code reviews do not have "why is this duration 280ms" comments. The system absorbs the small decisions.
Reference: Material 3 motion tokens document the same pattern. Apple Human Interface Guidelines on motion covers the platform-aware version for iOS. Both are studied references when scoping a custom system.
Four steps, clear use cases.
Quick 100ms. The threshold below which an interaction feels instant. Use for toggles flipping state, focus rings appearing, small icon swaps, button background color changes on press, checkbox checks. Anything that should not feel like an animation at all, but rather like the UI responding instantly to input.
Snappy 200ms. The default for most hover and active-state changes. Buttons growing on hover, color shifts on links, small badge appearances, dropdown menu opening one level. Feels responsive without feeling rushed. Most UI motion in a typical web app lives here.
Smooth 350ms. The default for surface changes that need to register as intentional. Modals appearing, drawers sliding in, tooltips fading, accordions expanding, dropdown menus with secondary panels, cards flipping. Slow enough that the eye tracks the motion; fast enough that the user does not wait.
Deliberate 600ms. For large-scale transitions: page transitions, hero element reveals on scroll, large layout shifts, cinematic moments. Use sparingly; if every interaction is at 600ms, the product feels sluggish. Anything beyond 600ms on the web feels broken; users start to think the app froze. Mobile devices typically need 10 to 15 percent shorter durations than desktop because touch latency adds 50 to 100ms before the user perceives the motion start.
Three curves, clear roles.
The universal "feels right" curve is cubic-bezier(0.2, 0.7, 0.2, 1). It accelerates quickly out of the start, holds a fast middle, then decelerates softly into the end. The asymmetric shape (heavy front, soft back) matches how physical objects move under impulse plus friction; it is the curve the eye expects.
For elements entering the scene (modals appearing, drawers sliding in, content fading in), use ease-out or its equivalent cubic-bezier(0, 0, 0.2, 1). The motion starts fast and decelerates; the eye perceives the element "arriving" rather than "approaching." For elements exiting (modals closing, drawers retracting, content fading out), use ease-in or cubic-bezier(0.4, 0, 1, 1). The motion starts slow and accelerates; the eye perceives the element "leaving fast." The asymmetry between entering (decelerate-in) and exiting (accelerate-out) is the convention; reversing it feels strange.
Linear is reserved for continuous motion: spinners, progress bars, marquees, infinite carousels. The constant velocity is correct for repeating motion and would feel wrong for any one-time transition. Avoid ease-in-out for hovers and short transitions; the slow-fast-slow shape makes the motion feel mushy, especially at 200ms or less. easings.net visualizes the common named curves; cubic-bezier.com lets you craft and preview custom curves.
Brand personality maps to easing in subtle ways. A "deliberate, considered" brand reads as slightly longer durations (350 to 600ms) with the standard cubic-bezier curve. A "fast, energetic" brand reads as shorter durations (100 to 200ms) with a sharper accelerating curve like cubic-bezier(0.4, 0, 0.2, 1). The differences are small in absolute terms but accumulate across hundreds of interactions in a product.
Eight lines, all surfaces.
The Tailwind transition-duration and transition-timing-function utilities take custom values via theme.extend. In tailwind.config.js for Tailwind 3, or in the CSS-first @theme block for Tailwind 4, define transitionDuration keys named quick, snappy, smooth, deliberate mapped to 100ms, 200ms, 350ms, 600ms.
Define transitionTimingFunction keys named standard, enter, exit, linear mapped to cubic-bezier(0.2, 0.7, 0.2, 1), cubic-bezier(0, 0, 0.2, 1), cubic-bezier(0.4, 0, 1, 1), linear. Optionally add brand-specific variants like brand-snappy for a sharper accelerating curve. Eight lines of config total, plus any brand variants.
In components, write class="transition-all duration-snappy ease-standard hover:bg-moss-500" instead of class="transition-all duration-200 hover:bg-moss-500". The semantic naming reads clearly in code review (a reviewer sees "snappy hover" not "200ms hover" and immediately understands the intent). When the brand team decides to slow down all hovers, you update the snappy token from 200ms to 250ms and every hover updates automatically.
For more complex animations (keyframes, multi-step choreography), Framer Motion on React or Motion One on vanilla JS extends the pattern. Both libraries accept the same duration and easing tokens; pass them as props rather than hardcoding values. The same Tailwind tokens drive both CSS transitions and JS-animated sequences. Single source of truth.
Respect the OS, pass WCAG.
Operating systems let users signal they prefer reduced motion. On macOS: System Settings, Accessibility, Display, Reduce motion. On iOS: Settings, Accessibility, Motion, Reduce Motion. On Windows: Settings, Ease of Access, Display, Show animations. On Android: Settings, Accessibility, Remove animations. The browser exposes this preference via the prefers-reduced-motion media query.
The web app must respect the preference. WCAG 2.2 SC 2.3.3 (Animation from Interactions) requires that any non-essential motion can be disabled. Implementation: wrap every transition declaration in a @media (prefers-reduced-motion: no-preference) block so the animation only applies when motion is allowed, or add an override @media (prefers-reduced-motion: reduce) block that drops all transition-duration to about 0.01s. Avoid 0s exactly; some scripts watch transitionend events and 0 duration never fires the event.
Tailwind exposes motion-safe and motion-reduce variants. The motion-safe variant applies a class only when no preference is set; the motion-reduce variant applies only when reduce is requested. Pattern: class="motion-safe:transition-all motion-safe:duration-smooth motion-reduce:transition-none". Components declare their motion safely without per-component @media queries.
The accessibility audit will check this. axe-core, Lighthouse, and most third-party audit tools flag missing prefers-reduced-motion support. A production design system has to ship with the motion-safe and motion-reduce variants baked into every animated component, or the audit fails. The cost is one extra utility class per declaration; the benefit is no audit fines and a better experience for the 5 to 10 percent of users (estimates vary) who have the preference set. Related reading: Color system WCAG compliance, Brand book vs design system, Modular type scale guide.
Six answers.
What are motion tokens and why do they matter?
Motion tokens are named, system-wide values for animation duration and easing. Without them, every component picks values arbitrarily. With them, motion is consistent across hover, modal, drawer, page transition. The system replaces ad-hoc decisions.
What's the right duration scale for a 2026 web app?
Four steps: quick 100ms (toggles, focus rings), snappy 200ms (hovers, micro-feedback), smooth 350ms (modals, drawers, tooltips), deliberate 600ms (page transitions, hero reveals). Anything beyond 600ms feels broken on the web.
Which easing curves work for UI motion?
cubic-bezier(0.2, 0.7, 0.2, 1) is the universal feels-right curve. ease-out for elements entering (fast start, soft land). ease-in for elements exiting. Linear only for spinners and progress bars. Avoid ease-in-out on hovers; it feels mushy.
How do I implement motion tokens in Tailwind 4?
In tailwind.config.js (or the CSS-first config), extend theme.extend.transitionDuration with custom keys mapped to ms values, and transitionTimingFunction with named cubic-bezier strings. Then build utilities like duration-snappy ease-standard and use them across components.
How do I respect prefers-reduced-motion automatically?
Use the @media (prefers-reduced-motion: reduce) query to drop durations to about 0.01s. Tailwind exposes motion-safe: and motion-reduce: variants. Required for WCAG 2.2 SC 2.3.3; hard-fail accessibility audits ignore this.
Should motion be in the brand book or the design system?
Both. Brand book defines motion personality (deliberate vs snappy, organic vs mechanical) as a few sentences. Design system encodes that personality as the actual token values. Components reference tokens, never raw ms or cubic-bezier strings.
One scale, every surface.
We design motion tokens, encode them as Tailwind 4 theme keys, retrofit them across React components, and ship the prefers-reduced-motion audit pass. Scoped quote in 48 hours.
Published .