Motion
Create motion with managed keyframes, duration tokens, easing tokens, shorthands, and reduced-motion conditions.
Overview
Motion explains what changed, where attention should go, and whether an action is still in progress. In CSS, that often means animations, transitions, and @keyframes, but the design decision comes first: use motion to give feedback, preserve continuity, clarify hierarchy, or reduce surprise.
Master CSS keeps those decisions close to the element with animate:* recipes, native animation:* and transition:* shorthands, shared duration tokens, shared easing tokens, managed @theme keyframes, and motion preference conditions such as @motion and @reduce-motion.
Choose motion tokens
Start with the job of the motion, then choose the smallest recipe, duration, and curve that communicates it. Most product UI needs short, quiet motion; long loops and large movement should be reserved for progress, live status, or expressive moments.
Animation recipes
Animation tokens package a named animation recipe. Use animate:* when the preset or project token should own the full animation value.
| Token | Class | Role / Description |
|---|---|---|
--animate-fade | animate:fade | fade 1s infinite Opacity reveal or exit when layout should stay steady. |
--animate-flash | animate:flash | flash 1s infinite Temporary attention signal; avoid for persistent states. |
--animate-float | animate:float | float 3s ease-in-out infinite Ambient lift for decorative or lightweight emphasis. |
--animate-heart | animate:heart | heart 1s infinite Positive feedback for likes, favorites, or celebratory moments. |
--animate-jump | animate:jump | jump 1s infinite Playful upward emphasis for expressive interfaces. |
--animate-ping | animate:ping | ping 1s infinite Beacon or live-status pulse around an anchor. |
--animate-pulse | animate:pulse | pulse 1s infinite Breathing feedback for loading, live, or waiting states. |
--animate-rotate | animate:rotate | rotate 1s linear infinite Continuous spinner or progress indicator. |
--animate-shake | animate:shake | shake 1s infinite Error or invalid-input attention; keep it short. |
--animate-zoom | animate:zoom | zoom 1s infinite Scale reveal for popovers, dialogs, and emphasized entrances. |
animate:* resolves to animation: var(--animate-*), so updating the token changes every element using that recipe.
Duration scale
Duration tokens describe rhythm. They are shared by animation and transition utilities, so the same token can make an entrance, hover state, delay, or transition feel like part of one system.
| Token | Class | Role / Description |
|---|---|---|
--duration-fastest | animation-duration:fastest | 75ms Micro feedback such as pressed states and tiny affordances. |
--duration-faster | animation-duration:faster | .1s Quick exits, icon feedback, and very short state changes. |
--duration-fast | animation-duration:fast | .15s Popovers, fades, short entrances, and hover feedback. |
--duration-normal | animation-duration:normal | .2s Default interaction transitions when no stronger rhythm is needed. |
--duration-slow | animation-duration:slow | .3s Standard UI movement and visible state changes. |
--duration-slower | animation-duration:slower | .5s Panels, drawers, and larger reveals. |
--duration-slowest | animation-duration:slowest | .8s Ambient or emphasized motion that should be used sparingly. |
Easing scale
Easing tokens describe the curve. Use custom easing tokens for product motion language and native CSS values such as linear, ease-in-out, steps(), or cubic-bezier() when a one-off curve is clearer.
| Token | Class | Role / Description |
|---|---|---|
--easing-smooth | animation-timing-function:smooth | cubic-bezier(.4, 0, .2, 1) Balanced movement for common UI transitions. |
--easing-soft | animation-timing-function:soft | cubic-bezier(.33, 1, .68, 1) Gentle reveals and quiet fades. |
--easing-crisp | animation-timing-function:crisp | cubic-bezier(.16, 1, .3, 1) Quick feedback with a polished finish. |
--easing-snap | animation-timing-function:snap | cubic-bezier(.2, 0, 0, 1) Firm settling for compact controls. |
--easing-accelerate | animation-timing-function:accelerate | cubic-bezier(.4, 0, 1, 1) Exits or elements leaving the screen. |
--easing-decelerate | animation-timing-function:decelerate | cubic-bezier(0, 0, .2, 1) Entrances or elements arriving on screen. |
--easing-overshoot | animation-timing-function:overshoot | cubic-bezier(.34, 1.56, .64, 1) Playful scale or position emphasis. |
--easing-rewind | animation-timing-function:rewind | cubic-bezier(.36, 0, .66, -.56) Pulled-back exits and reversals. |
--easing-spring | animation-timing-function:spring | cubic-bezier(.68, -.6, .32, 1.6) Expressive emphasis; use sparingly. |
Namespaces for motion
Motion tokens use focused namespaces so recipes, durations, and easing curves can be mixed inside explicit animation and transition classes.
| Group | Utility keys | Description |
|---|---|---|
| Animation recipes | animate | Use full animation recipes from animate tokens. |
| Duration | animation, animation-duration, animation-delay, transition, transition-duration, transition-delay | Share timing tokens across animations, transitions, and delays. |
| Easing | animation, animation-timing-function, transition, transition-timing-function | Share curve tokens across animation and transition timing functions. |
Keep duration and easing token names readable because they appear inside shorthand class names such as animation:fade|fast|smooth and transition:opacity|normal|standard.
Use motion tokens
Use animate:<token> when the full recipe should come from an --animate-* token.
<div class="animate:fade"></div><div class="animate:zoom"></div>Generated CSS
@layer theme { :root { --animate-fade: fade 1s infinite; --animate-zoom: zoom 1s infinite }}@layer utilities { .animate\:fade { animation: var(--animate-fade) } .animate\:zoom { animation: var(--animate-zoom) }}@keyframes fade { 0% { opacity: 0 } to { opacity: 1 }}@keyframes zoom { 0% { transform: scale(0) } to { transform: none }}Use native animation:* shorthand when the animation name, duration, easing, direction, iteration count, fill mode, or delay should be visible in markup.
<div class="animation:fade|fast|smooth"></div><svg class="animation:rotate|slowest|linear|infinite@motion">...</svg><div class="animation:zoom|faster|overshoot|both"></div>Generated CSS
@layer theme { :root { --duration-fast: .15s; --easing-smooth: cubic-bezier(.4, 0, .2, 1); --duration-slowest: .8s; --duration-faster: .1s; --easing-overshoot: cubic-bezier(.34, 1.56, .64, 1) }}@layer utilities { .animation\:fade\|fast\|smooth { animation: fade var(--duration-fast) var(--easing-smooth) } .animation\:zoom\|faster\|overshoot\|both { animation: zoom var(--duration-faster) var(--easing-overshoot) both } @media (prefers-reduced-motion:no-preference) { .animation\:rotate\|slowest\|linear\|infinite\@motion { animation: rotate var(--duration-slowest) linear infinite } }}@keyframes fade { 0% { opacity: 0 } to { opacity: 1 }}@keyframes rotate { 0% { transform: rotate(-360deg) } to { transform: none }}@keyframes zoom { 0% { transform: scale(0) } to { transform: none }}Use explicit property utilities when only one part of the motion should change.
<div class="animation-duration:fast animation-timing-function:crisp"></div>Generated CSS
@layer theme { :root { --duration-fast: .15s; --easing-crisp: cubic-bezier(.16, 1, .3, 1) }}@layer utilities { .animation-duration\:fast { animation-duration: var(--duration-fast) } .animation-timing-function\:crisp { animation-timing-function: var(--easing-crisp) }}Use transition shorthands
Use transition:* for state changes. The property, duration, easing, and delay travel together, but the motion only runs when the state changes.
<button class="transition:opacity|fast|smooth">Save</button><div class="transition:transform|slow|overshoot|150ms">...</div>Generated CSS
@layer theme { :root { --duration-fast: .15s; --easing-smooth: cubic-bezier(.4, 0, .2, 1); --duration-slow: .3s; --easing-overshoot: cubic-bezier(.34, 1.56, .64, 1) }}@layer utilities { .transition\:opacity\|fast\|smooth { transition: opacity var(--duration-fast) var(--easing-smooth) } .transition\:transform\|slow\|overshoot\|150ms { transition: transform var(--duration-slow) var(--easing-overshoot) 150ms }}Duration and easing namespaces are shared across animation-duration, animation-delay, transition-duration, transition-delay, animation-timing-function, transition-timing-function, animation:*, and transition:*. Keep the token names readable because they appear inside shorthand class names.
Respect motion preference
Use @motion for non-essential motion so it only runs when the user has not asked the system to reduce motion.
<span class="animate:pulse@motion"></span>Use @reduce-motion to replace motion-heavy effects with a simpler state.
<div class="animation:zoom|fast|overshoot@motion animation:none@reduce-motion translate:0@reduce-motion"></div>Generated CSS
@layer theme { :root { --animate-pulse: pulse 1s infinite; --duration-fast: .15s; --easing-overshoot: cubic-bezier(.34, 1.56, .64, 1) }}@layer utilities { @media (prefers-reduced-motion:no-preference) { .animate\:pulse\@motion { animation: var(--animate-pulse) } } @media (prefers-reduced-motion:reduce) { .animation\:none\@reduce-motion { animation: none } } @media (prefers-reduced-motion:no-preference) { .animation\:zoom\|fast\|overshoot\@motion { animation: zoom var(--duration-fast) var(--easing-overshoot) } } @media (prefers-reduced-motion:reduce) { .translate\:0\@reduce-motion { translate: 0 } }}@keyframes pulse { 0% { transform: none } 50% { transform: scale(1.05) } to { transform: none }}@keyframes zoom { 0% { transform: scale(0) } to { transform: none }}Treat reduced motion as a parallel state, not an afterthought. Keep essential feedback visible through opacity, color, text, layout, or an instant end state when movement is removed.
Customize motion tokens
Override motion tokens in the project CSS entry when your product needs a different rhythm. Keep duration and easing names distinct because both namespaces are shared by animation and transition utilities.
@theme { --duration-enter: 180ms; --duration-exit: 120ms; --easing-standard: cubic-bezier(.4, 0, .2, 1); --easing-emphasized: cubic-bezier(.16, 1, .3, 1); --animate-dialog-in: dialog-in var(--duration-enter) var(--easing-emphasized) both; @keyframes dialog-in { from { translate: 0 0.5rem; opacity: 0; } to { translate: 0; opacity: 1; } }}<dialog class="animate:dialog-in transition:opacity|exit|standard"></dialog>Use named tokens for repeated motion decisions. Keep raw durations and raw curves for one-off timing that does not belong in the system yet.
Avoid motion noise
Everything loops, moves far, and uses playful easing.
<button class="animation:jump|slowest|spring|infinite">Save</button>The element moves only when the state change needs feedback.
<button class="transition:transform|fast|crisp">Save</button>Duration and easing names overlap.
@theme { --duration-smooth: 200ms; --easing-fast: cubic-bezier(.4, 0, .2, 1);}Names keep their namespace meaning clear.
@theme { --duration-fast: 150ms; --easing-smooth: cubic-bezier(.4, 0, .2, 1);}Build responsive layout systems with containers, columns, gutters, breakpoints, container sizes, spacing tokens, CSS Grid, and Flexbox.
Choose fluid dimensions, measured sizes, container caps, constraints, and responsive sizing rules for stable layouts.