Motion guide
Motion is the product, so the rules are strict: springs for things that move, short fades for things that change, nothing that blocks reading.
Springs
| Use | Motion config |
|---|---|
| Indicators, active pills, layoutId | { type: "spring", stiffness: 420, damping: 32 } |
| Presses | whileTap={{ scale: 0.96 }}, stiffness 520, damping 26 |
| Panels, sheets, accordions | { type: "spring", stiffness: 260, damping: 28 } |
| Toasts, popovers | { type: "spring", stiffness: 380, damping: 30 } |
| Digit reels | { type: "spring", stiffness: 160, damping: 20 } |
Durations and easings
For plain CSS transitions the theme ships spring curves as linear() easings:
geturui.css
1--ease-out: cubic-bezier(.22, 1, .36, 1); /* fades, blur, colour */
2--ease-spring: linear(0, .009, .035 2.1%, …, 1); /* ~16% overshoot */
3--ease-spring-soft: linear(0, .06, .22 8%, …, 1); /* ~4% overshoot */
Durations: 140ms hover colour, 220ms small moves, 420ms panels, 560ms springs. Use them as Tailwind classes: transition-transform duration-500 ease-spring.
Patterns
- Press: scale to 0.96 in 90ms, spring back.
- Hover lift: cards rise 3px; arrows nudge 3px.
- Swap: content changes crossfade with an 8px blur rather than sliding.
- Reveal: one word-by-word reveal per page, on the H1. Don't animate every section on scroll.
- Indicator: a single element that glides between options (
layoutId), never one per option fading in and out.
swap.tsx
1<AnimatePresence mode="popLayout" initial={false}>
2 <motion.span
3 key={label}
4 initial={{ opacity: 0, y: 6, filter: "blur(8px)" }}
5 animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
6 exit={{ opacity: 0, y: -6, filter: "blur(8px)" }}
7 >
8 {label}
9 </motion.span>
10</AnimatePresence>
Performance
Animate transform, opacity and filter. Height changes go through Motion's layout or height: "auto", never per-frame state. Don't animate box-shadow on scroll.
Reduced motion
Every item respects prefers-reduced-motion: large movements become fades and loops stop. In your own code, use Motion's hook:
example.tsx
1const reduce = useReducedMotion();
2<motion.div animate={{ x: reduce ? 0 : 120, opacity: 1 }} />