Motion
One curve, the glide (--ease-glide, no overshoot), and a few durations, applied to every piece by its data-slot in consistency/motion.css; a box whose content changes follows it with useGlide.
consistency/motion.cssUse case
The pieces already move: a button sinks to 0.96 on press, a check pops in, a switch knob and a tab's pill slide, menus grow in from their trigger, the command bar and the comboboxes glide to their new height as you type. Reach for this when a layout changes content of its own: a list filtered by a view, a row added to an order, a condition added to a filter, a message that appears in a card. Put useGlide on the box that holds the content and it follows every change, its top edge fixed, the rows sliding, leaving blurred and arriving sharpening. Take any other curve or duration from the tokens; nothing in the library bounces.
Use it when
- A box whose content changes size: useGlide on the box, data-glide-item on its rows
- Something that moves to a new place, such as a pill or a knob (ease-glide, --dur-slide or --dur-tick)
- Something that appears in place (ease-glide, --dur-enter, from opacity 0 and a few pixels)
- A press on something the layout draws itself (ease-press down in --dur-press, ease-glide back)
Use something else when
- Something that opens over the page: its motion comes with the piece: Dialog
- A panel that opens and shuts: its height is the piece's own: Collapsible
- Content that is still loading: Skeleton
- A short wait on an action: Spinner
Examples
- Bistro Le MarchandMontréal
- Ferme des ÉrablesSaint-Hyacinthe
- Boulangerie Rive-SudLongueuil
- Quincaillerie DupuisQuébec
Pick a view: the card glides, its top fixed; rows slide, leave blurred, arrive sharpening.
Slowed to 520ms here so the shape shows. One curve, no overshoot: nothing in the library bounces.
Code
Import
import { useGlide } from "@/hooks/use-glide"
/* the curves are tokens.css, the pieces' motion is motion.css; both load once in app/globals.css */Usage
{/* a list filtered by a view: the card follows it, top edge fixed */}
const glide = useGlide<HTMLDivElement>()
<div ref={glide} className="overflow-hidden rounded-[18px] bg-background p-2">
<SegmentedControl value={view} onValueChange={setView} options={VIEWS} />
<ul>
{rows.map((r) => (
<li key={r.id} data-glide-item>{r.name}</li>
))}
</ul>
</div>
{/* something appearing in place, on the glide */}
<li className="transition-[opacity,translate] duration-(--dur-enter) ease-glide starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none">
Order 1049, Atelier Nord
</li>Rules
- One curve: ease-glide (cubic-bezier(0.32, 0.72, 0, 1)) for everything that moves, grows, shrinks or slides; ease-press only for the finger going down. Never a spring that overshoots, never ease-in, never linear except a progress that is time itself.
- Durations are tokens: --dur-press 130ms, --dur-color 160ms, --dur-popup 160ms, --dur-tick 200ms, --dur-enter 240ms, --dur-slide 300ms, --dur-glide 360ms. A size or a position following content may take the full 360ms because nothing waits on it; anything a person waits on stays under 300ms.
- A box that changes height keeps its top edge fixed and grows downward: anchor anything that floats by its top, never by its bottom, so a card that gains a row does not climb.
- A box whose content changes gets useGlide (hooks/use-glide.ts, lib/glide.ts outside React) and overflow-hidden; its rows carry data-glide-item (library rows are found by their data-slot). Never animate height by hand with a fixed number.
- A pill under a picked tab or segment slides to it (TabsList does it); never swap a fill from one tab to the other.
- A piece gets its motion from its data-slot in consistency/motion.css: never add transition classes to a piece.
- Name the exact properties (transition-[opacity,translate]), never transition-all; animate transform, opacity and filter, and height only through the glide.
- Respect reduced motion: the glide applies the change at once; elsewhere motion-reduce:transition-none, or keep only the fade.
- Hover effects only behind (hover: hover) and (pointer: fine); press feedback on :active.