Skip to content

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

--ease-outcubic-bezier(0.23, 1, 0.32, 1)

Everything entering. The house curve.

--ease-in-outcubic-bezier(0.77, 0, 0.175, 1)

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.

WhatDurationHow
Press feedback160ms`:active { transform: scale(0.97) }`
Colour settle150msEvery link and control, applied at the base rather than per call site
Fade300msOpacity-only changes
Page and nav enter480ms`opacity` plus a 10px `translateY`, once per navigation
Standings bar fill280ms30ms stagger per row
Bar chart mount800ms`scaleX()` from `left center`
Line and area draw900msRecharts `animationDuration`
Logo marquee40slinear, 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 transform and opacity. Never width, height or layout.
  • Never animate a keyboard-triggered action. A tab switch changes content instantly.
  • Never enter from scale(0). Start at scale(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

A tab switch changes content instantly: no slide, no fade on the panel. Someone arrowing through a tab list hits the animation once per keypress, and at that rate it stops reading as polish and starts reading as lag.

Only transform and opacity

Animating width, height or any layout property forces the browser to re-lay-out every frame. A bar chart fills via 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.