Skip to content

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.

A considered transition

Combine tokens: arrive over 220ms with ease.out, leave over 160ms with ease.in. Reduced motion changes the state instantly.

TokenValueUsed for
duration.instant0.1sHover and press color changes
duration.fast0.16sFades, exits, validation messages
duration.base0.22sHeight changes, such as an accordion
duration.slow0.36sLarge 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.