Motion
A small, durable motion system for the Design Manager. It borrows the rigor of a premium motion foundation — easing by job, duration by size, reduced-motion always — and applies a soft, buttery feel: every move decelerates to a gentle stop rather than snapping. Motion here is a quiet instrument that tells the rep what just happened and where it came from.
The law
Motion is information. Every transition must answer one of two questions — what just happened? or where did this come from? If it answers neither, it’s decoration, and decoration gets cut. This is Principle 6 (“restrained motion — a workbench, not a dashboard demo”) made operational.
Concretely: a status flips instead of hard-swapping, a filtered row glides to its new spot so you don’t lose it, a reply rises from the composer so you know where it came from. The feel is buttery — most motion lands in 210–560ms on a soft decelerating curve, so nothing snaps and nothing drags. The only looping motion in the whole tool is the single live “now” step. No spring bounce on data, no hero flights, no blurred scrims — just smooth and dependable.
Motion is information
It explains what happened or where something came from. If it does neither, remove it. No motion for delight’s sake.
Smooth, never abrupt
Motion glides to a stop on a soft decelerate — 150–560ms by size. Buttery, not bouncy; generous, but never so slow it makes the rep wait.
Ease by job, never linear
out for entering, in for leaving, in-out for moving between two on-screen states. Linear reads as cheap.
Animate the change, not the element
When a chip flips Draft→Published, tween the colour. Don’t fly the whole card around. The smallest honest move wins.
Restraint scales
A list of 100 rows animating fully is chaos. Cap stagger, reveal once, never loop decoration. Density is the priority.
Reduced motion always
One media query, honored on every surface. Skipping it is careless and makes people motion-sick. Non-negotiable.
Containers settle, never snap
When a panel’s contents change count, the box glides to its new height and everything below reflows with it. A container that jumps size reads as broken. Resize is motion too.
Motion speed
One knob, three paces. Everything below ships at a buttery default we call Smooth — but some reps want it out of the way, and some want it unhurried. So the whole motion layer is a single front-end preference: Fast, Smooth, or Calm. It’s remembered, and it carries across the tool.
Speed is a time axis, not a redesign. The easing curves never change — every move still decelerates to the same soft, buttery stop. All a pace does is stretch or compress how long the move takes, so the character is constant from Fast to Calm; only the tempo moves.
Smooth is the anchor — the locked default. Fast collapses durations toward instant so motion just confirms the change and gets out of your way. Calm stretches them out for a quieter, more deliberate pace. Pick once; it sticks.
Now set to Smooth — content swap 280ms · tab / route 560ms · list settle 400ms · per-row stagger 55ms. Your choice is saved and applied everywhere — refresh, or open another page, and it holds.
Fast
Almost no motion — lightning-fast, motion just confirms & leaves.
Smooth
The anchor. Buttery, generous but never sluggish. The locked default.
Calm
Unhurried — generous glides, a quieter and more deliberate pace.
The three scales
Pick duration by how BIG the move is. Anchored on the approved 280ms card-hover decelerate. Fast and Calm paces rescale every duration; easing stays constant.
| Token | Fast | Smooth | Calm | Use |
|---|---|---|---|---|
--dur-instant | 70ms | 150ms | 240ms | Toggle, single dot flip |
--dur-fast | 90ms | 210ms | 340ms | Hover colour, focus, small state change |
--dur-base | 120ms | 280ms | 460ms | Chip morph, content swap, card hover |
--dur-slow | 170ms | 400ms | 660ms | List settle (FLIP), larger surfaces |
--dur-morph | 210ms | 520ms | 840ms | State-morph commit (publish) — deliberate |
--dur-tag | 260ms | 700ms | 1120ms | Status-tag colour crossfade — slow, even reveal |
--dur-page | 200ms | 560ms | 900ms | Route / tab transitions, smooth scroll |
--dur-curve | 300ms | 760ms | 1180ms | Easing-player dot — long enough to FEEL the curve |
--dur-grow | 420ms | 1100ms | 1700ms | Shared-surface growth — slow enough to track the edges from card to detail |
--dur-roll | 720ms | 1800ms | 2900ms | Value count roll — long dramatic settle |
--dur-scroll-min | 220ms | 520ms | 800ms | In-page scroll, short hops |
--dur-scroll-max | 420ms | 1100ms | 1700ms | In-page scroll, long hauls |
--stagger | 20ms | 55ms | 95ms | Stagger step for lists/grids (cap the count) |
Easing is identical across all three paces — the seven curves never move. Only time scales, so the buttery character is preserved end to end. Smooth is the bare default in the generated stylesheet — it adds nothing, so it can never drift from the spec.
One attribute, one source of truth
- The selector writes
data-motion="fast | smooth | calm"on<html>. - The registry-generated stylesheet swaps the duration scale for that attribute. Smooth is the bare
:rootdefault. - Every CSS transition already reads
var(--dur-*), and the JS-driven motions (value roll, smooth scroll) read the same tokens live viareadDurationMs— so both stay in lockstep, no second code path.
Why a pure time axis
- Changing easing per pace would change the character of the tool — a different feel, not a different speed. The curves stay fixed so Fast and Calm are unmistakably the same system.
- The button press scales too: its dip-and-glide timing tracks the pace, so a click never feels out of step with the rest.
- It’s a preference, set once and remembered — not a per-action setting. Quiet, like everything else here.
Easing — by job
Pick easing by what the element is DOING. The curve says what kind of move it is: out for entering, in for leaving, in-out for moving between two on-screen states. All curves decelerate for the buttery glide, none are linear, and none change with pace. Click a runner to feel its curve.
--ease-standard cubic-bezier(0.32, 0.72, 0, 1)Workhorse — smooth, soft landing
--ease-out cubic-bezier(0.22, 1, 0.36, 1)ENTERING — buttery quint decelerate
--ease-in cubic-bezier(0.64, 0, 0.78, 0)LEAVING — eases up, accelerates away
--ease-in-out cubic-bezier(0.76, 0, 0.24, 1)MOVING between two on-screen states
--ease-settle cubic-bezier(0.34, 1.40, 0.64, 1)RARE tiny overshoot — confirm / commit only
--ease-glide cubic-bezier(0.16, 1, 0.30, 1)SIGNATURE long expo decelerate — press & state-morph commits
--ease-smooth cubic-bezier(0.45, 0, 0.55, 1)EVEN symmetric — colour crossfades that reveal gradually
out fast then settles — entrances · in eases up then accelerates away — exits · in-out symmetric — things repositioning on screen · standard the workhorse · settle a 1.4 overshoot reserved only for a successful confirm/commit, never for
routine data · glide the signature long expo decelerate — press release & state-morph
commits · smooth even and symmetric — colour crossfades that reveal gradually. The
runners ride --dur-curve, a doc-only token long enough to FEEL each curve.
Duration — by size
Pick duration by how BIG the move is — anchored on the approved 280ms card-hover decelerate. Feedback is near-instant; bigger surfaces get more time. Click a row to watch its speed, change the pace above, and replay: every runner reads the same tokens, so they visibly rescale.
560ms is the ceiling, not a default. Three tokenised exceptions sit above it — --dur-tag (700ms, the slow even colour reveal), --dur-scroll-max (1100ms, long in-page scroll
hauls) and --dur-roll (1800ms, the value roll’s dramatic
settle) — each a deliberate long reveal of a change in place, never a layout move. --dur-curve exists only for this page’s easing player. Live values above are read
with readDurationMs and update the moment the pace changes.
Behaviours
The registry of every shippable motion recipe. Recipes only ever combine the tokens above — easing by job, duration by size. Where a behaviour is interactive, the real thing is embedded: click to play.
--dur-fast--ease-glide--ease-standardTactile press
Buttons & interactive cards dip on press — committing an ACTION. Asymmetric: a quick dip DOWN so it lands within the brief click, then a slow buttery glide BACK on release — never the mid-click reversal that reads as jitter. The press is for actions only: choosing between options is never a press — selection slides (see the sliding tab indicator).
Press them — a quick 210ms-class dip down, then a slow buttery glide back. The dip timing tracks the pace.
--dur-morph--ease-glideState morph
A status flip (Draft → Published) animates the change, never hard-swaps: width morph, tint crossfade, label crossfade and dot pulse choreographed across three beats.
Width morphs on --dur-morph / --ease-glide while the tint
and label crossfade — the change animates, the card never moves.
--dur-tag--ease-smoothStatus-tag crossfade
The chip is a COLOUR crossfade, so it rides the even symmetric curve over a long duration — a gradual reveal that never front-loads (which would read as an instant snap).
The flip is one state change — CSS on .km-chip rides --dur-tag (700ms) / --ease-smooth for the
gradual colour reveal.
--dur-slow--ease-in-out--ease-out--ease-inFLIP list settle
Filter transitions move surviving rows to their new homes (FLIP), with entering rows rising in and leaving rows accelerating away. Container height settles smoothly.
--dur-base--stagger--ease-outStagger + skeleton
Loading skeleton crossfades into content arriving as a quick cascade. Cap the stagger at 6 items — beyond that the tail feels broken, not choreographed.
Rows rise in via km-rise, 55ms apart, capped at 6 —
beyond that the tail feels broken, not choreographed.
--dur-page--ease-in-outSliding tab indicator
The active-tab underline travels to the clicked tab — one marker moving between two resting states. The segmented control thumb rides the identical recipe, including the ink view-toggle (Table | Cards) where the BLACK portion glides between options. Selection SLIDES, press DIPS.
One marker slides & resizes over --dur-page (560ms) with --ease-in-out — not three borders blinking on/off. The Seg thumb above
rides the identical recipe.
--dur-roll--ease-outValue roll
A figure that changes counts to its new value — gentle launch, long settle. The roll lands the moment the remainder falls under half a digit, so the figure never looks stopped and then flips once more.
Counts over --dur-roll (1800ms) read live with readDurationMs, on a long power-out so the last figures crawl to a
restful stop. Tabular figures — nothing shifts width.
--dur-scroll-min--dur-scroll-max--ease-in-outSmooth scroll
In-page travel is distance-scaled between the min and max scroll durations, easing in-out so the viewport glides rather than jumps.
--dur-base--ease-out--ease-settleDialog entrance
The scrim fades to its wash while the dialog rises (or zooms, or settles with the rare overshoot) — entering, so it decelerates in.
--dur-base--ease-outBubble entrance
Chat replies rise a few px and fade in (km-rise).
--ease-standardLive step breathing
The live "now" step in a checklist breathes quietly so the eye finds it without it shouting.
--dur-base--ease-outCard lift
On hover an interactive card lifts off the surface — scale 1→1.015, rise −3px, shadow deepens, border brightens. A physical object becoming active, never a bounce. PHANTOM-HOVER GUARD: a card resting under a stationary cursor (after a surface collapses) never lifts — hover is muted until the pointer actually moves, and the just-viewed card carries the lift instead.
--dur-base--ease-out--ease-inRoute transition
Views hand over like panels: the old page eases out (fade + 8px drop), the new one rises in. Rides the View Transitions API where available; cuts cleanly where not.
--dur-grow--ease-in-outShared layout morph
A card LITERALLY grows into its detail surface: one continuous element, FLIP-morphed from the small card’s exact footprint to the panel’s — the boundary is visible the whole way. Rides --dur-grow, the slowest UI move in the system; content fades in only after the surface is most of the way there.
--dur-fast--ease-standard3D tilt card
The card tilts ±4.5° toward the cursor with a touch of scale and a hover lift in the shadow — depth on demand, now clearly perceptible. Strictly rationed: one hero per view, never in dense lists.
--dur-tag--ease-outMeter fill with inertia
Progress fills from zero with a long ease-out instead of snapping, plus an optional whisper of sheen across the filled portion. The hero metric meter rides this on load.
--dur-base--stagger--ease-out--ease-smoothSkeleton with depth
Loading reads calm and confident: layered skeleton surfaces with a very soft shimmer, cards arriving on the stagger cascade, real content crossfading in, numbers rolling after.
--dur-base--ease-outCommand palette spawn
The palette spawns as a layer: glass panel scales 0.96→1 and rises 8px while the ink wash fades in. The scrim stays SHARP — the system’s no-blurred-spotlight law holds; only the floating glass itself frosts.
--dur-page--ease-in-outDrawer with parallax
The drawer slides in over the page while the page itself concedes the space — scaling to 0.985, shifting −12px, dimming a touch. Two real layers, honest depth, no blur.
--dur-morph--ease-settleStatus celebration
A tiny, contained reward on completion: the check draws itself, the badge settles in from 0.8 scale on the rare-overshoot curve, a soft tint halo fades. Never confetti.
--dur-fast--ease-standardCursor-follow glow
A barely-there radial wash tracks the pointer inside a premium surface — a glass-highlight core (derived from --glass-hi) blended into a faint brand tone, so the glow and the glass material read as one family. If you notice the glow before the content, it is too strong.
--dur-fast--dur-base--ease-standard--ease-settleIcon micro-motions
Every glyph owns one signature move with one grammar: HOVER previews it (bell swings, refresh half-turns, trash leans, upload rises, the accordion caret nudges where it will go), the glyph’s own EVENT plays it in full (wiggle on arrival, spin while syncing, hop on send, caret turn on open). Custom accent per glyph, identical behaviour everywhere.
--dur-base--ease-outSpatial modal
Opt-in depth for the dialog: as the glass arrives, the page behind concedes a breath of scale (0.98). The dialog reads as a layer above a physical surface, not a sticker on it.
--dur-roll--ease-in-outAmbient empty state
An empty state breathes instead of staring: the glyph drifts on a slow visible loop and the watch-dot pulses with an expanding ring — alive at a glance, quiet enough to live on screen all day.
--dur-tag--dur-roll--ease-smoothAI generation motion
Generating never shows a plain spinner: a soft brand-tone border sweep, a streaming text shimmer, a pulsing caret, and the button label morphing Generate → Thinking → Ready. Brand tones only — no rainbow gradients.
--dur-slow--ease-settle--ease-in-outSnap into place
Dragged or displaced items settle into their slot on the rare-overshoot spring — released cards snap home, displaced neighbours glide aside via FLIP. Physical, decisive, brief.
What we don’t animate
Knowing what to leave still is what keeps the tool calm. These are good patterns in a consumer app and wrong here — included so the restraint is a decision, not an omission.
Spring overshoot on data
Bouncy springs are great for a draggable sheet; on a status chip or a row they make the tool feel toy-like and uncertain. Operational state lands flat. --ease-settle exists only for a committed confirm.
Hero image flights between routes
The “tap a card, watch it grow into the detail” move is lovely on a listings app. Here it draws attention to chrome instead of content, and slows a rep clearing twenty locations. Detail opens directly.
Full-page blur & dimmed spotlight scrims
Never frost or dim the whole page behind an overlay — it’s expensive, distracting, and off-tone for a workbench. The page stays sharp under a light wash. The floating surface itself may be translucent glass material — that’s contained material, not a page-wide spotlight.
Scroll-triggered reveals & parallax
This is a tool, not a landing page. Content is present when you scroll to it. Reveal-on-scroll belongs to design; here it just adds latency to reading a list.
Decorative loops
Exactly one thing loops: the live “now” step. Spinners, pulsing CTAs, shimmering banners — all banned. If something loops for attention, it had better be the single thing happening right now.
Reduced motion
Non-negotiable, and one media query. It ships in the component layer scoped to every product surface — animations collapse to an instant cut and nothing repositions, while the end state stays identical.
- The shipped product gates on it. In the real Design Manager, JS-driven
motion checks
prefers-reduced-motionand resolves to its end state — lists rearrange instantly, values set without rolling, bubbles appear without rising. - This spec page is the exception. Because its entire job is to show motion, the demos play on an explicit click even when the OS setting is on — clicking is consent. The product itself stays strict.
- End state is never sacrificed. Reduced motion removes movement, not meaning — the chip is still Published, the count is still right, the list is still sorted.
- Test it. macOS: System Settings → Accessibility → Display → Reduce motion.
Fast is not “reduced motion”
An easy thing to conflate, and worth keeping straight — they answer different questions and both stay honored.
Fast — a preference
A taste choice for people who want motion minimal and snappy. It’s still motion: a 90–200ms glide that confirms the change and leaves. The thumb still slides, the chip still morphs — just quickly.
Reduced motion — an accessibility need
The OS-level prefers-reduced-motion setting, for people who get motion-sick. It always wins: the component layer collapses animation to an instant cut on every surface, regardless of the pace chosen here. Meaning is never removed — only movement.
Order of precedence: reduced-motion (accessibility) → then the chosen pace. A Fast user with reduced-motion on still gets instant cuts; the pace only governs how the system moves when it is allowed to move at all.
Motion layer · motion is information; restraint scales — and Fast / Smooth / Calm scales the tempo
without touching the character. Generated from the registry: behaviours in registry/motion.ts, tokens in registry/tokens.ts.