Motion should explain, not perform.
Swivel motion is fast, anchored, and quiet. Animations exist to tell the user what happened — a state changed, a surface appeared, focus moved — never to entertain.
4.1
Principles
Three rules every motion decision answers to. If a transition violates one, cut it.
01
Purposeful
Every animation has a job — show a state change, anchor a new surface to its trigger, or guide attention to where the user needs to look next. No motion for its own sake.
02
Fast
Default to 150ms. UI motion shouldn't make the user wait. If a transition feels long, it is. Slow only when the surface is large enough to genuinely need it.
03
Anchored
Elements move from where they came from to where they're going. Menus grow from their trigger. Drawers slide in from their edge. No transitions that originate nowhere.
4.2
Duration scale
Five steps. Click any card to play it back at real time. The bar length is identical — only the speed changes.
80ms
Instant
--motion-instant
Tooltips, state flips that should feel instantaneous.
120ms
Fast
--motion-fast
Hover, focus, small color & opacity shifts.
240ms
Slow
--motion-slow
Expand/collapse, drawer slide, sheet, modal.
360ms
Deliberate
--motion-deliberate
Attention-getting changes, count-ups, hero reveals.
4.3
Easing
Five curves. The shape tells you the personality — flat ramps feel mechanical, curves with a fast start feel responsive. Click any card to scrub the puck across.
4.4
Property choices
Animate properties the GPU can handle. Avoid the ones that force layout — they jank on every frame.
Do · animate these
transform— translate, scale, rotateopacity— fadesfilter— blur, brightness (sparingly)background-color,color— token swapsborder-color,box-shadow— focus rings, elevationgrid-template-rowswith0fr → 1fr— expand/collapse without measuring
These compose on the compositor thread. No layout, no paint cost per frame.
Don't · animate these
width,height— usetransform: scale()or grid-rowstop,left,margin— usetransform: translate()padding— restructure or live with the snapborder-width— animatebox-shadowinsteadfont-size— usetransform: scaleat the parent
Every frame triggers layout. Janks on phones and dense tables. Cheap-looking, too.
4.5
State transitions
Interactive demos. Hover, click, expand — feel the timing live. The token is named under each.
4.6
Enter & exit
Surfaces that appear and disappear. Each pattern locks duration + easing + transform direction so they feel like one motion vocabulary.
task_altSaved · 12:04
Toast
240ms · --ease-enter · y-offset 8
Slide up from the anchor edge while fading in. Auto-dismiss exits with --ease-exit and 120ms.
Stop automation?
"Smart QPS · CTV Wave 4" will stop running and revert to the last saved baseline.
Dialog
240ms · --ease-emphasized · scale 0.96
Scrim fades, dialog grows from 96% scale + 12px lift. Exit uses --ease-exit at 150ms.
Smart QPS · CTV Wave 4
Pacing guard · Display Q3
Brand safety patch · v12
Bid throttle · House deals
List stagger
240ms · --ease-enter · 40ms stagger
Rows fade up in sequence. Use sparingly — only on first paint of a meaningful list, not on filter changes.
4.7
Reduced motion
Honor
prefers-reduced-motion on every interaction. The information is the same — just delivered without the transit.What stays
State changes still happen — color flips, opacity, chevron rotations. Tooltips still appear. The user gets every signal they would have gotten.
What goes
Translates, scales, slide-ins, list staggers, decorative loops. Anything where the motion itself is the message.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
/* Keep color/opacity legible — just remove the transit */
.toast, .dialog { transform: none !important; }
}
4.8
Do & don't
The shortlist. If you find yourself reaching for "but it looks cool" — close the tab.
Do · move from the trigger
Menus grow from their button. Drawers slide from the edge they're docked to. The user's eye follows the motion to where the new content lives.
Do · pair durations to size
Small thing → 120ms. Card-sized → 150ms. Drawer or modal → 240ms. Match the duration to the distance the user's eye has to travel.
Do · transition both directions
If something animates in, it must animate out. Snapping shut after a smooth open feels broken — even if it's faster.
Don't · bounce
No spring overshoot in product UI. We're not a confetti app. Save bounce-easings for marketing surfaces, never the workspace.
Don't · stack animations
If a row is being added and the table is being filtered and a toast is showing — pick one to animate. The others should snap.
Don't · loop without purpose
A spinner is a status indicator — it should appear when there's work and disappear when there isn't. Never decorative idle loops on empty states.