Motion
Quiet motion: three speeds on one curve, what moves and how, and reduced motion.
Motion is quiet. It shows that something arrived, opened or changed, and then it gets out of the way. Three speeds on one curve cover everything; most of it comes with the components.
Three speeds, one curve
The bigger the thing that moves, the longer it takes: a color change is quick, a menu is a little slower, a drawer slower still. Everything uses one curve, fast out of the gate with a gentle landing, so the whole product moves alike.
The three speeds
Press Play. Each dot crosses at its speed, on the same curve.
Quick 150ms
Hover, color, press
Base 200ms
Menus, sections, toasts
Slow 300ms
Drawers, sheets, dialogs
| Speed | Time | Class | For |
|---|---|---|---|
| Quick | 150ms | duration-150 | Hover, color, press |
| Base | 200ms | duration-200 | Menus, sections, toasts: small things opening |
| Slow | 300ms | duration-300 | Drawers, sheets, dialogs, page entrances: big things arriving |
| The curve | cubic-bezier(0.23, 1, 0.32, 1) | ease-out | Everything. The package sets `ease-out` to it. |
What moves, and how
| Moment | Speed and motion | Done by |
|---|---|---|
| Color or border changes under the pointer | Quick | transition-colors duration-150 |
| A button pressed | Quick: shrinks to 98% | Button does it |
| A clickable card hovered | Quick: rises 2px with a soft shadow | Card does it |
| A menu, tip or popover opens | Base: fades in | the component does it |
| A hover card opens | Base: waits 300ms, then fades and grows in; closes 150ms after the pointer leaves | HoverCard does it |
| A section opens or closes | Base: slides to its height; what's below moves, never jumps | Collapsible does it |
| A toast arrives | Base: drops in from above | Toast does it |
| A drawer, dialog or bottom sheet opens | Slow: slides or fades in over a dimming page | the component does it |
| Cards appear when a page loads | Slow: fade up 12px, 50ms apart, once | the app, with Framer Motion or animate-fade-in-up |
| A chart draws | Slow: bars grow from the baseline, once | on first show or a range change, never on a refresh |
Reduced motion
- When someone turns on reduced motion, the package stylesheet makes every CSS transition and animation instant. Loading spinners keep spinning: they say something is happening.
- JavaScript animations aren't covered by that rule. With Framer Motion, check
useReducedMotion()and skip the movement (show the content at once). - Nothing moves on its own for more than five seconds without a way to pause it, and nothing flashes more than three times a second.
Using motion
- Use the three speeds and
ease-out. Noduration-500,duration-[250ms]or other curves in the app. - Animate once. Don't replay an entrance on hover, on polling or on every re-render.
- Content that loads late must still show: animate it in unconditionally, not only when scrolled into view, or it can stay invisible.
- Never animate to decorate or to pull the eye: no bouncing, pulsing or looping attention-grabbers.
- Move with
transformandopacity. Animating width, height or top makes the page stutter (Collapsible's height is the one exception, measured by Base UI).
// CSS: the speed and the curve
<div className="transition-opacity duration-200 ease-out" />
// Framer Motion in an app: the same values, and reduced motion honoured
import { useReducedMotion, motion } from "framer-motion";
import { transition } from "@brdgapp/ui/lib/motion";
const still = useReducedMotion();
<motion.div
initial={still ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={transition("slow")}
/>Rules
Quiet motion
Motion marks arrivals and presses, not decoration. Respect reduced motion: CSS animations are cut by the stylesheet, and JavaScript animations check useReducedMotion(). Loading spinners keep turning.