04 · MOTION · DRAFT

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.
150ms
Base
--motion-base
The default. Reach for this first.
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.
Standard
cubic-bezier(0.2, 0, 0, 1) · --ease-standard
The default. Used for enter, exit, and almost every state transition. Snappy start, gentle settle.
Emphasized
cubic-bezier(0.3, 0, 0, 1) · --ease-emphasized
Large surface transitions where the motion itself is meaningful — drawers, modals, expand/collapse.
Enter
cubic-bezier(0, 0, 0, 1) · --ease-enter
Element appearing on screen. Decelerates into place — feels like it's settling into its anchor.
Exit
cubic-bezier(0.3, 0, 1, 1) · --ease-exit
Element leaving. Accelerates out — get out of the user's way quickly. Pair with shorter durations.
Linear
linear · --ease-linear
Continuous, mechanical motion — progress bars, video scrub, spinners. Never for state transitions.
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, rotate
  • opacity — fades
  • filter — blur, brightness (sparingly)
  • background-color, color — token swaps
  • border-color, box-shadow — focus rings, elevation
  • grid-template-rows with 0fr → 1fr — expand/collapse without measuring
These compose on the compositor thread. No layout, no paint cost per frame.
Don't · animate these
  • width, height — use transform: scale() or grid-rows
  • top, left, margin — use transform: translate()
  • padding — restructure or live with the snap
  • border-width — animate box-shadow instead
  • font-size — use transform: scale at 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.
Hover · press
120ms · --ease-standard
Color flip on hover, 3% scale-down on active. Confirms the press without a delay.
Select
120ms · --ease-standard
Background + border swap on selection. No scale, no bounce — just the color change.
Expand · collapse
240ms · --ease-emphasized
Grid-rows 0fr → 1fr unlocks height without measuring. Chevron rotates 180°.
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.