Case study

Motion System

Motion you tune by eye, not by number. Springs described as how long and how bouncy, a playground where every token races its siblings, and a small set of rules that keep the feel once it's right.

Role
Product design, design system, front end
Timeline
2026
Stack
React 19, TypeScript, Motion, Tailwind v4
Links
Live · GitHub
The Tokens section of the playground mid-race on a dark graphite page: the four duration tokens, instant, quick, base and slow, each with an orange dot at a different point along its track, the easing tokens below, and a Controls panel with time scale and reduce-motion settings
Every token in a family starts together, so you can watch which one arrives first. The controls change the whole page at once.
01 · Problem

Everyone can feel bad motion. Nobody can name it.

A menu that lands too hard, a toast that leaves slower than it arrived, a bounce that reads as cartoonish: people notice, but there are no words to fix it with. So motion gets magic numbers, a 300ms here and an ease-in-out there, tuned by whoever built that screen, and every screen feels slightly different.

I wanted motion you could talk about and judge by eye: names like snappy and gentle instead of stiffness values, and a place to watch the options side by side before choosing one.

02 · Decisions

Rules about how things should feel.

I tried a knob for every token, then removed it. Early on, every token had its own control. I took them out. Easing tokens are roles (standard, enter, exit), so letting a viewer swap the exit curve for something else broke what the token means. Editing came back later behind an Edit button. This time it changes values, not roles, and it sits right next to the race it affects.

03 · Redesign

The first version looked like every docs site.

v1 was neutral on purpose, with Inter on off-white, one card per token, and a temporary switcher previewing four visual directions. It worked, but it was generic. It had the docs-site look the brief listed as the first thing to avoid.

Version 1: a plain heading 'How things move', three pill links for the layers, and a grid of identical white cards for each duration token, with a theme switcher at the bottom
Before, there was one card per token and no point of view.
The redesign in dark graphite: a medium-weight headline above a full-width instrument with a dark grid screen and four flat keys, each topped with a thin bar in its token family's color
After the redesign, it became a quiet graphite instrument with one key and one color per token family.
  • Races instead of cards.Thirteen separate cards became four panels, where every token in a family runs on the same track at the same moment. You see the difference instead of comparing numbers.
  • Color means token type.Duration is orange, easing blue, spring yellow, stagger green, from the hero keys to every demo. Every color pair passes WCAG AA.
  • One type family.Space Grotesk at two weights carries the whole hierarchy. A second sans would have looked like a near miss.
  • Quiet gray, loud tokens.Three steps of gray separate the screen, the page and the panels, and outlines are hairlines. The four token colors are the brightest things on the page, so the motion carries the personality.
04 · Structure

Components ask for a name, never a number.

The Primitives section of the playground on dark graphite: cards for Reveal, Presence, Stagger and Move, each with a live demo stage, a Replay or Hide button, a one-line description and the code to use it
Each primitive in the middle layer has a live demo and the code to use it.

The layers exist for one reason: once a feel is approved, no screen can quietly tune its own. A component can only ask for a name.

05 · How I checked it

Measure it, don't trust it.

06 · Outcome

Tuned by eye, then locked in.

The playground is where a feel gets decided: run the races, edit a spring until it looks right, then export it as JSON, TypeScript or CSS. From there it ships as a name, so the motion you approved is the motion every screen gets.

← All work Open the playground