04 · foundations
10 tokens · 4 componentsRadius & motion.
Soft, never round. Motion with a job: entrances, counters and light that follows the cursor, each one earning its place. Every animation has a reduced-motion path.
Border radius
5 tokens--radius-sm6px · badge
--radius-md10px · button, input
--radius-lg16px · card, dialog
--radius-xl24px · featured
--radius-full9999px · avatar, dot
Motion
5 tokens- --motion-fast120mshover, tooltip
- --motion-base200mscard, button, a menu opening
- --motion-slow320mssheet, collapse, theme switch
- --ease-outcubic-bezier(0.2, 0.8, 0.2, 1)entrances and feedback
- --ease-in-outcubic-bezier(0.4, 0, 0.2, 1)a height that opens and closes
The global reset collapses every CSS animation and transition to about 0ms for people who ask for less motion. Components that animate through JavaScript call useReducedMotion() on their own.
Motion components
4 components3 of them animate through JavaScript. They share lib/motion.ts: the ease-out curve, one viewport rule and a stagger cap of six. The motion package installs with them and nowhere else.
- RevealEntrance that rises and fades in once on screen, with a capped list stagger
- TypeInText that assembles piece by piece, with the full sentence always in the DOM
- RollingNumberOdometer counter that rolls into place and turns once on hover
- ArrowLinkText link with a traveling arrow and a brand rule that wipes in · CSS only
Motion in CSS
7 classesFor what has to move before JavaScript runs, such as a loading screen or an overlay. They live in globals.css, and the reduced-motion reset stops them.
.type-linea line that types itself with no JS, set --type-chars.type-fadea line that fades up after --type-delay.type-careta blinking block caret.type-latea line that only shows when a wait runs long.skeleton-sweepone band of light over a grid of skeletons.motion-fade, .motion-pop, .motion-sheetan overlay or a sheet opening and closing.motion-collapsea panel opening to its height, handed over as --collapse-height
Motion rules
from the rules- Trigger entrances with whileInView and once, everywhere. Use animate above the fold. One trigger leaves no judgement call to get wrong.
- Ask useReducedMotion() in anything animated through JS. Assume the CSS reset caught it. Motion never goes through that block.
- Keep the real text in the DOM and animate a hidden copy. Grow a string in state. Crawlers and screen readers get an empty heading.
- Move glows and shines with transform. Rebuild a gradient every frame. It repaints the card and stalls other animations.