Skip to content

Reference · motion

Timing: durations, easing, stagger, springs

motion/references/timing.md96 linesupdated 16 Sept 2026

1. Duration by use

Interaction Duration Token Measured / source
Keyboard-driven UI (⌘K, list highlight, key-switched tabs) 0ms --duration-instant Raycast has no palette animation; Emil Kowalski
Link color, hover color/bg 100–150ms --duration-fast Linear links 100ms, Vercel 150ms
Button press (:active) 100–160ms --duration-fast Linear 160ms scale .97
Tooltip 125–150ms --duration-base or literal 125ms Emil 125–200
Dropdown, popover, select, tabs 150–250ms --duration-base Stripe nav 240ms, Anthropic dropdown 200ms
Accordion 200–360ms --duration-base--duration-slow Stripe accordion 360ms
Dialog, toast 200–350ms --duration-slow Emil modal 200–300
Drawer / sheet 300–500ms literal 450ms + ease-drawer Vaul 500ms
Page transition 250–400ms --duration-slow--duration-slower perf rule PERF-56 ≤ 400ms
Marketing entrance / reveal 400–700ms --duration-slower Supabase 400, Stripe 500, Granola/Attio 600, Mercury 670
Hero choreography total ≤ 1000ms Linear H1 1000–1500 (upper bound, don't exceed)
Ambient pulse / shimmer 1.5–6s loop literal pulses 2–6s
Marquee 30–180s loop literal Resend 180s, Supabase 30–35s
Spinner rotation 600–800ms/turn literal faster spin feels faster

Reconciling "UI < 300ms" with longer marketing entrances: they are different token groups. Components the user operates repeatedly stay ≤ 300ms (fast/base/slow). One-time, explanatory marketing motion may use --duration-slower (300–600ms depending on direction). lint/long-ui-duration flags interaction feedback over 500ms.

Exit = 70–80% of enter, and half the travel. Closed-state rules carry --duration-fast, open-state rules --duration-slow, so exits are faster automatically.

Distance: add ~50ms per extra 100px of travel beyond 100px. Mobile full-screen sheets 300–400ms; desktop equivalents 20–30% shorter.

Direction personality (already in the token values): precision quick and exact; editorial slow fades, no scale; swiss linear and near-instant (--ease-out: linear, fast = 0); warm springy panels; terminal steps() and 60–100ms; instrument minimal.

2. Easing decision flow

text
Entering or exiting the screen?          → ease-out            (never ease-in)
Moving/morphing while staying on screen? → ease-in-out         (tab indicator, reorder, carousel slide)
Simple color / opacity hover?            → ease (CSS keyword) or ease-out
Constant motion or progress?             → ease-linear         (marquee, hold-to-confirm fill, spinner)
Sheet / drawer?                          → ease-drawer
Marketing entrance or reveal?            → ease-out-expo
Gesture release with velocity?           → spring (motion)     { type: "spring", duration: 0.4, bounce: 0 }
Success / playful moment (warm only)?    → ease-spring-bouncy  (or bounce 0.15–0.25)
Otherwise                                → ease-out

Don't invent cubic-beziers. The measured top-site curves are already the tokens: (.23,1,.32,1) out-quint (Linear, Vercel, Raycast), (.19,1,.22,1) / (.16,1,.3,1) out-expo (8 sites), (.32,.72,0,1) drawer (7 sites), (.77,0,.175,1) in-out-quart (6 sites). Tailwind's default ease-out (0,0,.2,1) is too weak and is overridden by void.

3. Travel and scale

Motion Start state
Popover / menu opacity 0, scale(.96) from trigger origin
Tooltip opacity 0, scale(.97)
Dialog opacity 0, scale(.96), centered origin
Toast translateY(100%) from its edge
Drawer translateY(100%)
Reveal opacity 0, translateY(8–16px) (reveal uses 12px) or masked translateY(105%)
Hero H1 translate 0.3em, no opacity
Press scale(.97) (surfaces .99)
Hover lift translateY(-2px) max

Never scale(0), never travel > 40px for reveals (measured 8–24px), blur ≤ 4px on small elements only.

4. Stagger

Case Step Cap
Hero lockup (eyebrow, H1, sub, CTAs) 60ms 4–5 items
Card/list group on first reveal 40–60ms 6 items / 300ms total; the rest share the last delay
Per word 20–40ms one sentence
Per character avoid (15–30ms if unavoidable; one DOM node per glyph) a single short word

Stagger never blocks input, never replays on filter/sort/re-render, and collapses to 0 under reduced motion (base zeroes durations and delays).

5. Springs (motion library)

Feel Config
UI (sheets, layout) { type: "spring", duration: 0.35–0.45, bounce: 0 }
Gesture release { type: "spring", duration: 0.5, bounce: 0.1 } keeps velocity when interrupted
Playful (warm success) { type: "spring", duration: 0.5, bounce: 0.2 }
Pointer-follow (decorative) useSpring(value, { stiffness: 300, damping: 30 })

Near-critical damping. An overdamped spring (damping 60) took 5s to settle in the owner's app and was the top idle CPU cost. No springs on opacity or color. CSS springs: ease-spring (300/30, no overshoot, pair with ~300ms) and ease-spring-bouncy (400/24, ~9% overshoot, ~450ms).

6. Reduced motion

Motion type Under prefers-reduced-motion: reduce
Press, hover color keep (≤ 100ms, not spatial)
Popover / dialog / toast opacity only or instant (base zeroes base/slow)
Reveals, parallax, scrubbed scenes, marquees removed, final frame shown
Page transitions none
Canvas / WebGL one static frame
Progress, spinners keep (they convey state), slower or static bar

CSS: base.css handles durations; recipes wrap spatial animation in @media (prefers-reduced-motion: no-preference). JS: or matchMedia("(prefers-reduced-motion: reduce)") read in an effect (server snapshot false).