Motion
Emboss components don't pick easing curves — they pick a spring.
The five springs
/**
* The shared spring vocabulary. Every animated component draws its physics
* from here — inline spring numbers are not allowed anywhere else, and the
* CSS `linear()` easings in the token stylesheet are generated from these
* same constants, so CSS and JS motion stay physically identical.
*/
export const springs = {
/** Fast attack with no wobble — the downstroke of a press. */
press: { type: "spring", stiffness: 1100, damping: 70, mass: 1 },
/** Springy return with ~6% overshoot — the machined release. */
release: { type: "spring", stiffness: 420, damping: 26, mass: 0.9 },
/** Confident glide for sliding carriages, tabs, and thumbs. */
settle: { type: "spring", stiffness: 550, damping: 34, mass: 1 },
/** Soft rise for floating surfaces. */
float: { type: "spring", stiffness: 320, damping: 28, mass: 1 },
/** Near-instant snap for detents and ticks. */
detent: { type: "spring", stiffness: 1400, damping: 76, mass: 1 },
} as const;
export type SpringName = keyof typeof springs;The same constants generate the CSS linear() easings in the token stylesheet (ease-release, ease-settle, ease-float), so a CSS transition and a JS spring are physically identical. The release curve carries a ~6% overshoot — that machined snap-back you feel when a button lets go.
CSS owns depth, JS owns displacement
Hover lifts, focus blooms, press travel, and overlay entrances run in pure CSS — cheap and SSR-safe. The motion library is reserved for things CSS can't express: sliding carriages that track layout, knob rotation, meter needles, and drag-to-dismiss.
Reduced motion
Under prefers-reduced-motion, durations collapse to nothing — but depth state changes still happen instantly. Depth is a state signal, so it never disappears; it just stops moving.