Motion

Generated from DESIGN_SYSTEM.md

Rules

Motion explains a change; it never decorates one. Something moves because it entered, left, moved or responded, and the motion shows where it came from and where it went. Nothing moves on its own except a loading indicator and a designated ambient canvas (Motion canvas below).

Duration scale — four steps

StepWebUse
150msduration-150Color and opacity on small controls: a pressed button, a toggled chip
200msduration-200Inputs and hover: focus ring, field border, row highlight
300msduration-300 (the default)Surfaces: a card lifting, a dialog or sheet opening, the sidebar drawer
500msduration-500Large reveals: a page-level section or a hero entering
  • The default is 300ms. Pick a step from the table; a value off the scale (duration-[250ms], duration-700 on a control) needs a reason written beside it.
  • Leaving is faster than entering. A dismissed surface uses the step below the one it opened with, so the interface never waits on its own exit.
  • Longer than 500ms is ambient only: marquee, skeleton shimmer, a progress meter filling to its value. Never for something the person is waiting to use.

Easing — three curves

CurveWebUse
Ease outease-out (the default)Anything entering or responding to input: it starts fast and settles
Ease in and outease-in-outSomething moving between two resting places: a drawer, a sheet, a tab indicator
Linearease-linearContinuous loops only: a spinner, a marquee

Bare ease-in is not used for interface motion: it starts slowly, which reads as lag.

Platforms

PlatformDefaultRule
Webtransition-* duration-300 ease-outEntrance and exit animations are real transitions or framer-motion; a class that generates no CSS is not motion
iOS.snappy(duration: 0.25, extraBounce: 0) for controls; .spring(response: 0.32, dampingFraction: 0.88) for surfacesSystem presentation (sheets, alerts, navigation) keeps its own motion
AndroidMaterial 3 motion defaults (tween/spring from Compose)System presentation keeps its own motion

Reduced motion

  • Every non-essential animation stops under reduced motion — prefers-reduced-motion on the web (useReducedMotion() or motion-safe:), Reduce Motion on iOS (accessibilityReduceMotion), animator duration scale 0 on Android.
  • A moving surface becomes a cross-fade or an instant change; a loop stops on a still frame; a skeleton stays at rest.
  • Motion never carries meaning on its own: a state it announces is also stated in text, icon or color.

Motion canvas — decorative and waiting motion

Abstracted from reviewing thinking-orbs (Jakub Antalik, MIT), the reference for "free, but inside a flow" dots in Apple's setup screens. Motion that runs on its own — a brand background, an AI waiting state — is cheap by construction:

  1. Plain 2D canvas. No WebGL, no ctx.filter, no SVG filters: the same pixels in every browser, cheap on low-end phones. Device pixel ratio capped at 2.
  2. It stops when nobody sees it. Paused offscreen (IntersectionObserver) and in a hidden tab; instances share one clock and resume in phase.
  3. Reduced motion gets a still frame, never an empty box.
  4. Ink from the page: currentColor or the page theme, never a color literal in the canvas code.
  5. One component per job: the brand surface is BrandDotsCanvas; an AI waiting state is ThinkingIndicator (thinking-orbs, phase from the server task status, never a timer). A new moving surface extends one of them before it adds a third.

Live specimens

Four durationsPress Play: each box travels with its step on ease-out.
  • duration-150Pressed button, toggled chip

  • duration-200Focus ring, hover, field border

  • duration-300Surfaces: the default

  • duration-500Large reveals

Three curvesEase out enters or responds, ease in and out moves between two rests, linear only loops.
  • ease-out
  • ease-in-out
  • ease-linear