Motion
Every animated component draws from one small set of springs, durations and easings, and follows the same reduced-motion rule.
Principles
- Motion explains a change: where something came from, what it is attached to, or which way a value moved. If it explains nothing, it is left out.
- Responses to direct input are fast. Nothing a person triggers takes longer than about 300ms to settle.
- Things leave faster than they arrive. Exits are short fades so the interface never feels like it is waiting.
- Springs are used for anything that moves, because they can be interrupted mid-flight without a jump.
- Nothing loops or plays on its own, apart from loading indicators.
Springs
Springs are named for what is moving. Pick by the size of the thing and the distance it travels, not by how you want it to feel.
- spring.snappyThumbs, indicators, icons
- spring.smoothPopovers, dialogs, list items
- spring.gentleDrawers, lightboxes
Durations
For timed changes such as opacity and color. These tracks move the same distance to make each duration easy to compare.
- duration.instant 100msHover and press
- duration.fast 160msFades and exits
- duration.base 220msSmall reveals
- duration.slow 360msLarge reveals
Same distance and easing, different durations. Slow playback makes the small timing differences easier to see.
Easings
Duration sets how long a change takes. Easing sets how progress is distributed across that time. Compare all three curves at the same duration.
- ease.outFast start, gentle arrival
- ease.inOutGradual start and finish
- ease.inBuilds speed toward the end
Every dot travels for one second. The curves show how progress changes over time; the dashed diagonal is a constant speed.
Combining tokens
Pair a duration and an easing for a small reveal. Run the example in either direction to compare arrival and exit.
Combine tokens: arrive over 220ms with ease.out, leave over 160ms with ease.in. Reduced motion changes the state instantly.
| Token | Value | Used for |
|---|---|---|
| duration.instant | 0.1s | Hover and press color changes |
| duration.fast | 0.16s | Fades, exits, validation messages |
| duration.base | 0.22s | Height changes, such as an accordion |
| duration.slow | 0.36s | Large reveals, charts drawing in |
| ease.out | [0.22, 1, 0.36, 1] | Almost everything |
| ease.inOut | [0.65, 0, 0.35, 1] | Movement that starts and ends on screen |
| ease.in | [0.5, 0, 0.75, 0] | Rare. Elements leaving the screen |
Reduced motion
When the operating system asks for reduced motion, every component removes movement and scaling. What stays is anything that carries meaning:
- Opacity fades stay, because they do not move anything.
- State still changes. A switch still ends up on, a tab indicator still ends up under the new tab. It just gets there without travelling.
- Progress feedback stays, such as a loading spinner or the fill of a hold-to-confirm button.
- Movement that follows the finger stays. Dragging a drawer is the person moving it, not an animation.
Components do this with useMotionSafe. Its safe() function returns the transition unchanged, or an instant one when reduced motion is on.
import { motion } from "motion/react"
import { spring, useMotionSafe } from "@/lib/motion"
export function Indicator() {
const { reduced, safe } = useMotionSafe()
return (
<motion.span
layoutId="indicator"
transition={safe(spring.snappy)}
// Use `reduced` when the animation's values change, not only its timing.
initial={{ opacity: 0, y: reduced ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
/>
)
}CSS transitions
Simple hover, press and focus changes use CSS transitions rather than Motion, at 150ms. They are paired with motion-reduce:transition-none wherever they scale or move something.