Foundations
Motion
Two curves, six rules. Motion in Varick exists to make a state change legible, never to decorate one, so almost everything enters once, on mount, and then holds still.
Source of truth: varick-site/app/globals.css · varick-charts chart-spec.md §5
The two curves
Change the duration and replay to feel the difference. These are the only two easing functions in the system.
Both curves, 800ms
Everything entering. The house curve.
Anything that moves and returns.
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);--ease-out is the house curve: fast start, long settle. If you are unsure which to use, it is this one.
Press feedback
The one piece of motion that is on absolutely everything interactive.
transform: scale(0.97) over 160ms on --ease-out. Every interactive element gets this.
.press {
transition: transform 160ms var(--ease-out);
}
.press:active {
transform: scale(0.97);
}Durations
Fixed per thing, not per taste. A chart that animates for 900ms and a button that responds in 160ms are both correct.
| What | Duration | How |
|---|---|---|
| Press feedback | 160ms | `:active { transform: scale(0.97) }` |
| Colour settle | 150ms | Every link and control, applied at the base rather than per call site |
| Fade | 300ms | Opacity-only changes |
| Page and nav enter | 480ms | `opacity` plus a 10px `translateY`, once per navigation |
| Standings bar fill | 280ms | 30ms stagger per row |
| Bar chart mount | 800ms | `scaleX()` from `left center` |
| Line and area draw | 900ms | Recharts `animationDuration` |
| Logo marquee | 40s | linear, infinite, pauses on hover |
The rules
Emil Kowalski's principles are the house standard. These six are the ones that come up.
- Enter once, on mount. Nothing re-animates on scroll.
- Only animate
transformandopacity. Never width, height or layout. - Never animate a keyboard-triggered action. A tab switch changes content instantly.
- Never enter from
scale(0). Start atscale(0.95)with opacity if you must fade in. - Every interactive element gets press feedback.
- Honour
prefers-reduced-motion: render the final frame, do not just shorten the animation.
Never animate a keyboard-triggered action
Only transform and opacity
transform: scaleX() from a left center origin for exactly this reason.Reduced motion
Render the final frame. Do not just make the animation shorter.
Shortening a transition to 1ms is the lazy version and it still produces a flash. Under prefers-reduced-motion: reduce, standings bars start already filled and charts set isAnimationActive={false}, the reader gets the finished state, not a fast version of the entrance.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}The blanket rule above is the floor, not the whole job. Anything that animates a value into place, whether a bar, a counter or a drawn line, also needs a branch that renders the end state directly.
