|Design System

Design Tokens

Motion

Animation in this design system follows one principle: motion should serve the user, never entertain the developer. Every transition has a purpose, a curve, and a duration — chosen to feel fast, natural, and interruptible.

Easing Curves

Three custom cubic-bezier curves replace CSS defaults. Each serves a specific motion context.

--ease-out

cubic-bezier(0.23, 1, 0.32, 1)

Default for all UI transitions. Starts fast, settles gently.

When: Enters, reveals, hover responses, any element appearing on screen.

--ease-in-out

cubic-bezier(0.77, 0, 0.175, 1)

Symmetric movement. Accelerates then decelerates evenly.

When: Elements moving from point A to B (e.g., sliding panels, repositioning).

--ease-drawer

cubic-bezier(0.32, 0.72, 0, 1)

iOS-style pull feel. Quick launch, gradual settle.

When: Drawers, bottom sheets, slide-over panels.

Duration Scale

Duration is not arbitrary — it scales with frequency and importance.

ElementDurationRationale
Button press / hover100–160msMust feel instant. Users press buttons 100+ times per session.
Tooltips / small popovers125–200msFast enough to feel responsive, slow enough to register.
Dropdowns / selects150–250msOrigin-aware scale animation from trigger element.
Modals / drawers200–500msDeliberate reveals. Scale from 0.95, never 0.
Page transitions250–400msFade + subtle translate. Never block navigation.

Spring Presets

Framer Motion spring configs for physics-based animation. Springs feel natural because they respond to interruption — you can reverse mid-flight.

springSnappy

stiffness: 400, damping: 17

Buttons, CTAs, quick feedback. Feels decisive.

springGentle

stiffness: 300, damping: 25

Cards, hovers, layout shifts. Feels natural.

springBouncy

stiffness: 300, damping: 15

Playful interactions, celebrations. Feels alive.

Animation Decision Framework

Before adding an animation, answer these four questions in order.

1

Should this animate at all?

Frequency determines necessity. Actions performed 100+ times/day (typing, keyboard shortcuts) should NEVER animate. Rare actions (first login, onboarding) benefit most from animation.

  • Daily: typing, scrolling → no animation
  • Frequent: tab switching → minimal (150ms)
  • Occasional: modal open → moderate (250ms)
  • Rare: onboarding → expressive (400ms+)
2

What is the purpose?

Every animation must serve a function. If you can't name the purpose, remove it.

  • Spatial consistency — where did this come from?
  • State indication — what changed?
  • Explanation — how does this work?
  • Feedback — did my action register?
  • Preventing jarring changes — smoothing layout shifts
3

What easing?

Match easing to the motion's intent.

  • Entering screen → ease-out (--ease-out)
  • Moving between positions → ease-in-out (--ease-in-out)
  • Drawers / sheets → ease-drawer (--ease-drawer)
  • Drag / momentum → spring physics
4

How long?

Duration scales with the element's importance and frequency.

  • Micro-interactions: 100–160ms
  • UI transitions: 150–250ms
  • Full reveals: 200–500ms
  • Never exceed 500ms for UI

Rules

Non-negotiable animation principles. Break these and the UI feels off.

Never animate from scale(0)

Start from scale(0.95) + opacity: 0. Elements appearing from nothing look like a glitch. A subtle scale-up feels intentional.

transform: scale(0.95); opacity: 0; → scale(1); opacity: 1;

Always active:scale(0.97) on pressable elements

Every button, card, and interactive element needs a press state. 0.97 is subtle enough for desktop, perceptible enough for touch.

className="active:scale-[0.97]"

Never use ease-in for UI animations

ease-in starts slow and accelerates — it feels sluggish and unresponsive. Always use ease-out (fast start, gentle end) for elements entering the screen.

transition-timing-function: var(--ease-out);

Asymmetric timing: slow enter, fast exit

Enters are deliberate (the system presents). Exits are responsive (the system obeys). Enter at 250ms, exit at 150ms.

enter: 250ms ease-out → exit: 150ms ease-out

CSS transitions over keyframes for interruptible UI

CSS transitions can be interrupted mid-flight and reverse smoothly. Keyframes play to completion. Use transitions for anything the user might cancel (hovers, toggles).

transition: transform 150ms var(--ease-out);

Origin-aware popovers

Popovers, tooltips, and selects should scale from their trigger element (transformOrigin: top). Modals are the exception — they scale from center.

transform-origin: top; transform: scale(0.95);

Performance

Rules for keeping animations at 60fps under load.

Only animate transform and opacity

These are the only two properties composited on the GPU. Animating anything else (width, height, padding, margin, top, left) triggers layout recalculation and paint — visible as jank under load.

GPU-accelerated

transform, opacity

Triggers layout

width, height, padding, margin, top, left

CSS animations beat JS under load

When the main thread is busy (streaming AI responses, parsing large payloads), CSS animations continue smoothly because they run on the compositor thread. Framer Motion animations run on the main thread and will stutter. Use CSS transitions for critical UI feedback (button press, hover states) and Framer Motion for orchestrated sequences (stagger, spring physics, gesture-driven).

prefers-reduced-motion

Respect the user's system preference. Disable non-essential animations, keep functional state changes (opacity for visibility). Never remove motion entirely — users with reduced motion still need state feedback, just without physical movement.

@media (prefers-reduced-motion: reduce)