Motion

Every seamui animation comes from one file you own — lib/motion.ts. Springs over durations, depth over flatness, and a single dial to retune the whole library.

Change the feel in one line

All springs derive from springs, which just picks a personality. Swap the pick and all components change feel together — no component files to touch:

// lib/motion.ts
export const springs = personalities.seam     // the default
export const springs = personalities.brisk    // tighter, no overshoot
export const springs = personalities.relaxed  // softer, calmer
export const springs = personalities.playful  // more life everywhere

Each personality defines the same four roles — press (press-down feedback), snappy (release/settle), surface (overlays entering), and bouncy (accents). Pick one below and feel all four retune at once — that's the whole dial:

Press · press
Toggle · snappy
Overlay · surface
Accent · bouncy

Previewing personalities.seam — in your app this is one line in lib/motion.ts. Every component retunes together.

Want a custom feel? Edit the numbers or add your own personality to the table; every component picks it up automatically.

The tokens

Components never inline motion — they import tokens:

  • springs — the four spring roles above.
  • fades — opacity-only durations (fast/normal), the one sanctioned duration case besides layout dimensions.
  • depth — the virtual z-axis: pressed/resting/raised scalars plus overlay/modal enter–exit objects (for elements motion.dev controls end to end).
  • condense — the overlay CSS motion (below): surface, backdrop, sheet.
  • shake — error feedback, a brief horizontal shake.
  • reduced — the reduced-motion swaps (below).

Overlays — the condense

Every overlay (dialog, popover, dropdown, select, tooltip, sheet…) shares one motion: rise fast, settle alive; fall back quicker than you rose, with the backdrop dimming on the same clock. This one lives in CSS — the condense token — not a spring, because Base UI keeps a popup mounted through its exit and awaits CSS transitions before unmounting. It can't await motion's rAF springs, so a spring exit gets cut off instantly (the bug this fixes).

Popups grow out of their trigger (scale from Base UI's --transform-origin), modals pop from center, sheets slide up; all fade in, and reverse — a touch quicker — on dismiss. A spring-shaped bezier keeps the seam bounce. Under reduced motion the scale/slide is dropped and it's an opacity fade both ways, never dead.

Reduced motion is a variant, not a kill switch

When prefers-reduced-motion is on, seamui swaps movement for opacity — feedback never disappears. Press dims (reduced.pressed) instead of scaling, entrances fade (reduced.fadeIn) instead of rising, layout changes jump (reduced.instant) instead of springing, and the error shake becomes an opacity flash (reduced.flash).

Per-component overrides

Because you own the code, the escape hatch is the component file itself: change which token a component uses (e.g. springs.bouncy on a toast) rather than inlining a config — that keeps the personality dial working for everything.