← Case study

Detail 04 · Design system

How things move

The system's other half: 26 springs sorted into five named for their job, controls that answer alike, and nothing that pops. 7 minutes, 3 demos.

Try it: tap to sort 26 springs into five

01 · The idea

The other half of the system

The pencil language decides how the site looks: one ember, four typefaces, lines drawn by hand. It says nothing about how the site moves. How long a change takes, how much it bounces, what a button does under your finger, how something arrives and how it leaves: that’s half of what you feel on a page, and it needs rules as much as colour does.

Without rules, every new component gets tuned by feel, on the day it’s built. Each choice is fine on its own. Together they drift, and a page where every control has its own rhythm feels like parts from different kits.

02 · The audit

Twenty-six springs, seventeen settings

Before writing this detail I counted. The site had 26 springs, written one at a time across 19 files, with 17 different settings between them. Six demos carried their own identical copy of the same spring. The music player’s buttons and the demos’ sliders were both “a control”, tuned a hair apart.

None of it was wrong, which is why it lasted. Here is every one of them, plotted by how long it takes to settle and how much it bounces.

Every spring on the site

26 springs, 17 settings

Sorted by the job each one was doing, they fall into five groups, and inside each group the differences were noise: nobody could feel 0.2 against 0.25 of bounce, but every one of them had to be chosen, and copied, and kept.

03 · The code

Five springs, named for their job

Now there are five, in one file, and every animated change picks one by what it’s doing.

The site's springs
export const SPRING = {
  // A control answering the hand: a press, a toggle, a slider, an icon nudging on hover
  press: { type: "spring", visualDuration: 0.3, bounce: 0.2 },
  // Content changing in place: a label, a caption, a marker moving to the next row
  swap: { type: "spring", visualDuration: 0.35, bounce: 0.15 },
  // A shape becoming another, or travelling across the page: islands, the "you are here" box
  morph: { type: "spring", visualDuration: 0.55, bounce: 0.2 },
  // A small flourish meant to be noticed: dock icons on hover, a counter ticking up
  playful: { type: "spring", visualDuration: 0.35, bounce: 0.4 },
  // A value gliding to a new reading: bars, progress, a zoom
  glide: { type: "spring", visualDuration: 0.6, bounce: 0.1 },
};

The names are jobs, not feelings. A toggle is press wherever it lives, even inside a demo about islands; a label changing in place is swap, whether it’s in the page index or a demo’s tabs. The island’s spring is still exported as ISLAND_SPRING, but it’s now just a name for SPRING.morph, so the dock and the sidebar’s “you are here” box settle the same way.

Some springs stay where they are, on purpose. The ones that follow something (letters leaning toward the cursor, a reveal tied to the scroll) aren’t playing a change; they’re chasing a moving value, and each is tuned to what it chases. So are the per-letter springs that rewrite a word, where many small parts move at once.

04 · Controls

Three controls, one hand

Every control on the site answers in the same ways: it brightens or nudges under the pointer, does the same when the keyboard reaches it, pushes in when pressed, and turns ember when it’s on. Those states are the vocabulary. The spring is the accent they’re spoken in.

Press all three controls below at once. On one spring they come to rest together. Then give each the spring it might have got if it had been tuned alone.

Three controls, pressed at once

Press one, or all three.

Apart, nothing is broken. Each control is just a little too quick or too slow next to its neighbours, and the page stops feeling like one object.

05 · Nothing pops

In and out, never on and off

The other rule: anything a control adds or removes animates both ways. Add a note below, take one off, then switch the animation off and do it again.

Add a note, take one off

Without it, a note appears from nowhere and its neighbours jump to make room; take one off and the gap snaps shut. With it, the note blurs in on the swap spring and the others glide aside. The exit is the half that’s easy to forget: React removes an element at once, so it needs AnimatePresence to stay long enough to leave.

Leaving takes a wrapper
<AnimatePresence mode="popLayout">
  {notes.map((note) => (
    <motion.li key={note.id} layout transition={SPRING.swap}
      initial={{ opacity: 0, filter: "blur(4px)" }}
      animate={{ opacity: 1, filter: "blur(0px)" }}
      exit={{ opacity: 0, filter: "blur(4px)" }} />
  ))}
</AnimatePresence>

06 · Decisions

The rules of motion

Name it by its job

press, swap, morph, playful, glide. Choosing a spring is choosing what the change is, not how it should feel today.

Count before naming

The five came from the 26, not from a blank page. Tokens drawn from what’s already there fit what’s already there.

Followers stay local

A spring chasing the cursor or the scroll is tuned to its input. Sharing it would make it worse at its one job.

Exits count

Whatever arrives with motion leaves with it. A thing that fades in and blinks out feels half-built.

07 · Takeaways

What to keep

  • A design system covers motion too: how long, how bouncy, and what every control does under your hand.
  • Audit before you name: count the springs you have, sort them by job, and the tokens fall out.
  • Name springs by job, so picking one is a decision about what the change is, not a new round of tuning.
Five springs, everywhere→

The music island morphs open on morph, its buttons answer on press, the track name changes on its letter springs, and the progress line draws back on glide when you skip.