Skip to content
EMBOSS
Docs menu

Motion

Emboss components don't pick easing curves — they pick a spring.

The five springs

lib/springs.ts
/**
 * 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.