01

Confirm, don't perform.

Motion exists to confirm a change happened — not to entertain. If the user can't tell what changed, the animation has failed. If the animation is the point, it's too much.

02

Calm under pressure.

Quick on hovers (~120ms), considered on transitions (~200ms), deliberate on overlays (~320ms). Never rushed. Never showy.

03

Listen before moving.

Always honor prefers-reduced-motion. Replace movement with a state change, not nothing — the user still needs to see the result.

04

Earn the gesture.

Big moves (page transitions, hero reveals) should be rare and meaningful. If everything moves, nothing matters.

Four durations. Pick the smallest one that still reads as intentional.

Quick · 120ms

Hover · micro state

--motion-duration-quick

Hover color shifts, focus rings, small toggles. Should feel imperceptible but confirmed.

Standard · 200ms

Default state change

--motion-duration-standard

Most component transitions. Tab switches, accordion expand, button pressed → released, card hover lift.

Deliberate · 320ms

Overlay · expressive

--motion-duration-deliberate

Modal entrance, drawer open, toast slide-in. Long enough to feel composed without dragging.

Slow · 480ms

Page · hero

--motion-duration-slow

Page transitions, hero reveals, large illustrative moments. Use sparingly.

Easing carries personality. Standard for nearly everything; entrance for elements appearing; exit for elements leaving; emphasized only when something needs to pull attention.

Standard
cubic-bezier(0.2, 0, 0, 1)
Default. Use for most transitions.
Entrance · ease-out
cubic-bezier(0, 0, 0.2, 1)
Elements arriving on screen.
Exit · ease-in
cubic-bezier(0.4, 0, 1, 1)
Elements leaving the screen.
Emphasized
cubic-bezier(0.2, 0, 0, 1)
Drawing attention. Use rarely.

Reusable patterns. Compose by combining a duration and an easing.

fade-in
opacity 0 → 1
Standard · entrance ease. Use for content reveals where movement would be too much.
rise-in
translateY(12px → 0) + opacity 0 → 1
Standard · entrance ease. Toasts, dropdown menus, popovers.
slide-in
translateX(−24px → 0) + opacity
Deliberate · entrance ease. Drawer or sidebar reveals.
scale-in
scale(0.96 → 1) + opacity
Deliberate · emphasized ease. Modals, dialogs.
pulse
scale(1 → 1.4) + opacity 1 → 0
Notification dots, "live" indicators. Loop only when active.
shimmer
background-position scroll · linear
Skeleton loaders. Calm, never aggressive.

Hover is the most-frequent motion in the system. Keep it small.

SurfaceBehaviorTokens
Button — primaryBackground shifts darker (Intentional Blue → Steady Blue)--btn-primary-bg--btn-primary-bg-hover
Button — secondaryBackground fills with ink, label inverts to cream--btn-secondary-bg-hover
Card — defaultBackground lifts to --surface-muted--card-bg-hover
Card — themed (index)Inverts to Grounded Black on Clear WhiteUsed on Brand Guide Index
Link / nav12% Clear White overlay (on dark) · color shift to brand ink (on light)--state-hover-overlay-on-dark
Tag (interactive)Border color shifts to --ink-default; background unchanged

All hover transitions use --motion-hover (120ms standard easing).

Every component MUST honor prefers-reduced-motion: reduce. The state still has to change — just without the animation.

/* Always wrap motion in a media query */
@media (prefers-reduced-motion: no-preference) {
  .modal {
    animation: scale-in var(--motion-overlay-in);
  }
}

@media (prefers-reduced-motion: reduce) {
  .modal {
    animation: fade-in 0ms; /* state still flips */
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}