Motion With a Job to Do
Every animation here earns its place by answering one question for the person using the product: where did that come from, are these the same thing, did my tap register. No job, no motion. The system is deliberately small, three durations and five easings, so the whole product moves at one tempo: fast, and quiet.
Purposeful, Not Decorative
Good motion orients. It carries the eye from a tap to the panel that opened, shows a row slid instead of teleported, confirms a request landed. Decoration does none of that and costs the same frame time. Motion earns its place by clarifying a change, never by dressing one up.
- Orientation. A menu grows from the button that opened it.
- Continuity. A row animates to its new spot instead of jumping.
- Feedback. A control settles after a tap, confirming the press.
- Entrances that animate with nothing to orient.
- Looping, attention-grabbing movement with no status behind it.
- Long, cinematic reveals that make the interface wait.
The Three Durations
Match the duration to the size of the change. A small, local change should feel instant; a surface taking over the screen earns a beat. Nothing runs past 300ms, where responsive starts to feel slow. Hover a card to feel each one.
--duration-fastLocal state on a control the user is already watching: a hover tint, a focus ring, an icon toggling, a checkbox filling.
--duration-normalThe default. Things entering, leaving, or moving a short way: a dropdown, a tooltip, a tab indicator, an accordion.
--duration-slowA large surface that takes over the view and needs a beat to read: a modal, a bottom sheet, a full-screen overlay.
Larger surface or longer travel, longer duration. The ceiling is 300ms, and most motion lives at 200ms.
Five Easings, Five Jobs
Easing is the shape of speed over time, and it is where motion says something. Fast then settling reads as arriving; slow then accelerating reads as leaving. Pick the wrong one and the interface feels laggy or mechanical. Each curve below has one job.
--easing-linear Constant speed, no start or finish to shape. Only for continuous loops: spinners, indeterminate progress. On anything that begins and ends it feels robotic.
--easing-out Quick start, gentle landing. The default for anything entering or answering a tap. It moves at once, which reads as responsive, then decelerates into place.
--easing-in Slow start, accelerating exit. For elements leaving the screen, they gather speed and get out of the way. Never for entrances, a slow start there reads as lag.
--easing-in-out Eased on both ends. For an element moving between two on-screen positions, both visible the whole time: a reordering row, a toggle thumb, a tab indicator.
--easing-spring Overshoots, then settles. Reserved for a small, discrete success you want to feel alive: a checkmark, a like, a badge popping in. Comfort not information, so gate it behind reduced motion and keep it off large surfaces.
Anatomy of a Transition
Every transition names three things: the property, a duration token, an easing token. Name the property, never transition: all, which animates things you never meant to touch. Reference the tokens instead of hardcoding milliseconds, so the whole product keeps one tempo.
Performance Is Part of the Contract
A frame at 60fps is about 16ms. The browser can animate two properties, transform and opacity, on the compositor, off the main thread, with no layout and no repaint. Animating geometry recomputes layout every frame, and that layout thrash is the usual cause of jank. This is how the rendering pipeline works, not a preference, so the system animates transform and opacity by default.
Handled on the compositor, off the main thread. No layout, no paint. A slide is translateY, not a change to top.
Each frame recomputes layout, then repaints. On a long list or a weak device this drops frames. Express the same motion with transform.
Reduced Motion Is the Floor
Some people feel physically ill from on-screen movement. The browser exposes their OS setting as prefers-reduced-motion: reduce, a firm request to hold still that every animation honors. It aligns with WCAG 2.1: 2.3.3 asks that interaction-triggered motion can be disabled, and 2.2.2 covers looping motion.
Honoring it does not mean deleting meaning. Because information rides on color, opacity, and the final state, the interface stays legible without movement: collapse the animation, keep the destination. And keep a spinner turning slowly rather than freezing it, a stopped spinner reads as a hung app.
/* Default: motion is welcome. Enter fast, settle gently. */ .menu { transition: opacity var(--duration-normal) var(--easing-out), transform var(--duration-normal) var(--easing-out); } /* Firm request to hold still: collapse the movement, keep the meaning. */ @media (prefers-reduced-motion: reduce) { .menu { transition-duration: 1ms; /* appears, does not fly in */ transform: none; /* no slide */ } .spinner { animation-duration: 1.4s; /* slower, not gone */ } }
Rules of Motion
Each rule stands on its own: what to do, then why it holds. Read the why once and the rule stops feeling arbitrary.
Give every animation a job: orientation, continuity, or feedback.
Decorative motion clarifies nothing and costs the same frame time, so it is the first thing to cut.
Animate transform and opacity.
The compositor carries them off the main thread with no layout or paint. Animating width, height, top, left, or margin recomputes layout every frame and drops frames.
Name the property, a duration token, and an easing token explicitly.
transition: all animates properties you never meant to touch, and hardcoded milliseconds drift from the shared tempo.
Match the duration to the change: fast for local state, normal to enter or leave, slow for a takeover.
Anything past 300ms reads as sluggish, so the ceiling holds and most motion sits at 200ms.
Pick easing by job: out to enter, in to leave, in-out to move, linear to loop.
linear on anything with a start and an end feels robotic, and the wrong curve reads as lag or machinery.
Reserve spring for a small, discrete success, and gate it behind reduced motion.
An overshooting modal looks broken, and spring is comfort, not information.
Honor prefers-reduced-motion: collapse the movement, keep the meaning.
Motion alone must never carry meaning. Information rides on color, opacity, and the final state, so the interface stays legible without movement.
Token Reference
| Token | Value | Role |
|---|---|---|
--duration-fast | 150ms | Local state on one control: hover, focus, toggle |
--duration-normal | 200ms | Default. Enter, leave, or move a short distance |
--duration-slow | 300ms | A large surface taking over the view. The ceiling |
--easing-linear | cubic-bezier(0, 0, 1, 1) | Continuous loops only: spinners, indeterminate progress |
--easing-in | cubic-bezier(0.4, 0, 1, 1) | Elements leaving the screen |
--easing-out | cubic-bezier(0, 0, 0.2, 1) | Elements entering, and taps. The default |
--easing-in-out | cubic-bezier(0.4, 0, 0.2, 1) | Moving between two visible positions |
--easing-spring | cubic-bezier(0.34, 1.56, 0.64, 1) | Small discrete successes. Overshoots, then settles |
Sources
- Material 3, Motion. Duration buckets and easing sets; motion orients the user, maintains continuity, and gives timely feedback. m3.material.io/styles/motion
- Apple Human Interface Guidelines, Motion. Motion should be purposeful and communicate status; gratuitous animation is discouraged and Reduce Motion must be respected. developer.apple.com/.../motion
- web.dev and MDN, animation performance. Animate transform and opacity to stay on the compositor without layout or paint; use will-change sparingly because each promoted layer costs memory. web.dev/articles/animations-guide
- MDN, prefers-reduced-motion. A device-level preference to minimize non-essential motion, mapping to the OS accessibility setting. developer.mozilla.org/.../prefers-reduced-motion
- W3C WCAG 2.1. 2.3.3 Animation from Interactions (AAA) and 2.2.2 Pause, Stop, Hide (A) target vestibular symptoms such as dizziness and nausea. w3.org/WAI/WCAG21/.../animation-from-interactions
- The three durations (150ms, 200ms, 300ms), the five named easings, the 300ms ceiling, and gating spring behind reduced motion are specific to this system's tokens. SPA House decisions, consistent with but not dictated by the cited sources.