Skip to content

Motion

Like the colour tokens, motion is one shared vocabulary — a fixed set of durations, curves, and springs every animation is built from. JS and CSS are kept in lockstep by a generator and a drift test, so a curve can never mean two things. And because one provider wraps every app, every animation collapses under a reduced-motion preference by construction, not by remembering to.

Durations

Seven speeds, from instant to slow — watch the same distance covered at each. Quick for state, moderate for entrances, slow for ambient motion.

--duration-instant0ms
--duration-quick120ms
--duration-snappy220ms
--duration-normal250ms
--duration-swatch-fan300ms
--duration-moderate400ms
--duration-slow1000ms

Easings

The system's custom curve set. The dotted diagonal is linear; the solid curve is the token's real acceleration, and the dot rides it at the slow duration.

smooth easing curve
--ease-smoothcubic-bezier(0.4, 0, 0.2, 1)
customIn easing curve
--ease-custom-incubic-bezier(0.55, 0.085, 0.68, 0.53)
customOut easing curve
--ease-custom-outcubic-bezier(0.165, 0.84, 0.44, 1)
customExpand easing curve
--ease-custom-expandcubic-bezier(0.25, 0.46, 0.45, 0.94)
customGentle easing curve
--ease-custom-gentlecubic-bezier(0.25, 0.1, 0.12, 1)
snappy easing curve
--ease-snappycubic-bezier(0.22, 1, 0.36, 1)
swatchFan easing curve
--ease-swatch-fancubic-bezier(0.7, 0.7, 0.4, 0.6)

Four legacy aliases (--ease-standard, --ease-in, --ease-out, --ease-in-out) remain for className utilities, pending migration to the curve set above.

Springs

Eight physics-based springs, raced side by side. One clock releases every dot at once, so the personalities read against each other — stiffness, damping, and mass made visible.

springs.soft120 · 18 · 1

gentle, slow settle

springs.subtle300 · 30 · 1

barely-there, professional

springs.natural200 · 20 · 1

balanced default

springs.playful170 · 15 · 1

character with bounce

springs.bouncy260 · 12 · 1

visible, energetic bounce

springs.snappy400 · 28 · 0.8

quick and responsive

springs.stiff500 · 30 · 1

very quick, controlled

springs.swift500 · 30 · 0.5

instant, zero overshoot

Scales

Four transform steps for interactive feedback — press to shrink, hover to lift. Try them: each chip moves on the real token.

scales.press0.97 · on press
scales.pressDeep0.95 · on press
scales.hover1.02 · on hover
scales.hoverLift1.05 · on hover

Presets

Semantic compositions — the pairing an animation names instead of re-deriving. Each resolves to the duration and easing, or the spring, it is built from.

Dialogs

presets.dialogOpennormal × customOut
presets.dialogClosequick × customIn

Fades

presets.fadeInquick × customOut
presets.fadeOutquick × customIn

Slides

presets.slideInnormal × customOut
presets.slideOutquick × customIn

Hovers

presets.hoverLiftsprings.snappy
presets.hoverSettlesprings.natural

Singles

presets.snappysnappy × snappy
presets.swatchFanswatchFan × swatchFan
presets.shakenormal × customOut + shakeKeyframes

The rules

A shared vocabulary only holds if it is enforced. These are the four that keep every animation in the system speaking it.

Components animate with the m.* LazyMotion primitives, never the full motion.* namespace, so the animation engine is never forced into a consumer's bundle.

packages/ui/scripts/check-motion.mjs

ReducedMotionProvider wraps every app root; its MotionConfig reducedMotion="user" means every animation collapses under a reduced-motion preference by construction — no component re-checks the media query.

ReducedMotionProvider (MotionConfig reducedMotion="user")

Tokens only — no bespoke curves or durations. The CSS custom properties are generated from motion.ts, and a vitest drift-guard fails the build when the two diverge.

generate-motion-css.mjs + motion-css-sync.test.ts

One orchestrated moment per page — a single staggered entrance, and nothing more. No scroll-triggered animation, no parallax.

convention

In practice

Adoption looks like this: start from a token, override one axis, and leave a reason.

// packages/ui/src/components/sidebar/sidebar-motion-config.ts
// Based on springs.subtle with slightly reduced mass for a snappier feel.
export const SIDEBAR_TRANSITION = {
  ...springs.subtle,
  mass: 0.8,
} as const;

The pieces that speak the language most fluently — the tabs indicator, the segmented control, the sidebar — live in the component catalog.