A design system is a contract, not a component library
Why we treat tokens as the source of truth and components as a downstream artifact, not the other way around.
When a design system starts from components, every new screen becomes a negotiation. When it starts from tokens (color, type, spacing, radius), the components become the easy part.
The distinction matters because most teams believe they have a design system when what they have is a folder of components. The folder is useful. It is also the output, not the system, and the difference shows the first time somebody needs a screen the folder doesn't cover.
Components are downstream of decisions
A button component encodes decisions that were made somewhere else: this particular indigo, this radius, this amount of padding, this weight of text at this size. If those decisions only exist inside the component, then they only apply to buttons.
So when a designer needs a pill-shaped tag, or a card with a border, or a heading one step larger than any existing screen uses, there is nothing to consult. The values get chosen again, near enough to the originals to look intentional and far enough off to be visibly wrong on a page that shows both. Repeat that across a year of feature work and you get the drift that makes people ask for a redesign, when what they needed was a source of truth.
Start from tokens and the same request has an obvious answer. The tag uses the existing radius and the existing muted text colour. There is nothing to negotiate because the decision was made once.
What the tokens actually settle
A useful token set covers the things people otherwise invent: a colour palette with named roles rather than raw hex values scattered through the markup, a type scale with sizes and weights and letter-spacing bound together, a spacing scale, radii, and the one or two elevation levels a product is allowed to use.
Named roles matter more than the palette. --ink-secondary survives a change of
brand colour; --slate-600 does not, and neither does the twelfth grey somebody
added because none of the first eleven looked right.
The contract has two sides
This is the part that makes it a contract rather than a stylesheet.
Design commits to working within the scale, and to changing the scale rather than stepping outside it when something genuinely doesn't fit. A one-off value in a mockup is a request for an exception, and every granted exception makes the next one easier to justify.
Engineering commits to never hardcoding a value the token set already covers. A literal hex code in a component is a decision made outside the system, and it will not update when the token does. That is the moment a design system starts becoming a folder of components again.
Both halves are required. A team that honours only the first has documentation. A team that honours only the second has consistent implementations of inconsistent decisions.
How we build it
On this site the tokens live in CSS, not in a JavaScript config. Tailwind v4
reads them from an @theme block in app/globals.css, and the utilities we
compose with are declared next to them with @utility. There's no
tailwind.config.ts at all.
The reason is that CSS custom properties are the real source of truth at runtime. If the token exists as a CSS variable, then the utility class, an arbitrary value, a plain stylesheet and a browser inspector all see the same number. When tokens live in a build config and get compiled away, only the generated classes know about them, and everything else is guessing.
Keeping the token reference as a living document rather than a launch artifact
matters just as much. Ours is DESIGN.md, and it gets updated when the system
changes, because a token reference that has drifted from the code is worse than
none: people trust it and then are wrong.
Signs it's working
New screens get assembled rather than debated. A designer and an engineer look at the same mockup and disagree about layout, not about which grey.
A brand change becomes a small, reviewable diff instead of a project. If updating the primary colour means editing one declaration and looking at a handful of screenshots, the system did its job.
And the folder of components stays small. Not because you keep resisting new components, but because the ones you have compose, which is what happens when they were all built from the same set of decisions.