Skip to content

Motion

A fixed set of durations, curves and springs that every animation is built from. A generator and a drift test keep the JS and CSS values identical, and ReducedMotionProvider collapses every animation under a reduced-motion preference.

Durations

Five speeds, each covering the same distance. Quick for feedback and exits, normal for menus and dialogs, moderate for large surfaces, slow for ambient motion.

--duration-instant0ms
--duration-quick120ms
--duration-normal220ms
--duration-moderate400ms
--duration-slow1000ms

Easings

The dotted diagonal is linear; the solid curve is the token's acceleration. The dot rides it at the slow duration.

out easing curve
--ease-outcubic-bezier(0.22, 1, 0.36, 1)

Entering and leaving, and when unsure

inOut easing curve
--ease-in-outcubic-bezier(0.77, 0, 0.175, 1)

Moving between two visible positions

sheet easing curve
--ease-sheetcubic-bezier(0.32, 0.72, 0, 1)

Panels sliding in from an edge

Springs

Three springs released at the same moment. Each row shows how fast it arrives and how far it overshoots.

springs.standard0.3s · bounce 0

the default for interface motion

springs.swift0.2s · bounce 0

indicators and quick follow-ups

springs.settle0.35s · bounce 0.15

where a drag or flick throws something

Scales

Press to shrink, hover to lift. Each chip moves on its token.

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

Presets

Each preset is a named duration and easing.

Arriving and leaving

presets.enter
normal × out
presets.exit
quick × out

Feedback

presets.shake
normal × out + shakeKeyframes

Rules

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/oxlint.config.ts

ReducedMotionProvider wraps every app root. Its MotionConfig reducedMotion="user" collapses every animation under a reduced-motion preference, so 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 test 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. No scroll-triggered animation, no parallax.

convention

In practice

Pick the token by what the motion is for.

// packages/ui/src/components/sidebar/sidebar-motion-config.ts
// Opening and closing are ordinary interface motion.
export const SIDEBAR_TRANSITION = springs.standard;

// A drag throws the panel, so the snap earns a small overshoot.
export const SIDEBAR_MAGNETIC_TRANSITION = springs.settle;

The tabs indicator, the segmented control and the sidebar show it in use.