Motion
Five durations, three easings and a small set of enter and exit presets. Motion explains where things come from and where they went, and it gets out of the way when people ask for less.
Presets
FadeTooltips, overlays, content swapping in place.
animate-fade-in / animate-fade-outRiseNew rows, toasts, inline messages, anything that appears in the flow.
animate-rise / animate-sinkScalePopovers, menus and dialogs, from their trigger.
animate-scale-in / animate-scale-outSlide from endDetail panels and sheets that open beside a list.
animate-slide-in-end / animate-slide-out-endSlide from startGoing back a step, navigation drawers.
animate-slide-in-start / animate-slide-out-startGrowBars and meters filling to their value. Also animate-grow-y.
animate-grow-xDurations
Every preset reads its duration from a token. When the system setting asks for reduced motion, the tokens drop to 1ms: state changes still happen, nothing moves. Looping effects like the AI shimmer and indeterminate progress stop on their own.
--duration-instant100msExits of small things, hover feedback--duration-fast150msPopovers, menus, most exits--duration-base240msDefault enter, panels, rows--duration-slow400msLarge surfaces, page sections--duration-slower640msData reveals: bars, charts, metersRules
Enter with ease-out at the base duration. Leave with ease-in, faster than you came in. Never animate layout on scroll or keep something moving that carries no meaning.
<div className="transition-colors duration-(--duration-fast) ease-out hover:bg-accent" />
{/* Enter and leave with a preset */}<div className="animate-rise data-closed:animate-sink" />For measured or reordered elements, use the same tokens from JavaScript. lib/motion.ts installs with the theme.
import { animate, duration, useReducedMotion } from "@/lib/motion"
// Web Animations API with the same tokens. Resolves at once with reduced motion.await animate(row, "rise")await animate(row, "rise", { direction: "exit" })