1 · Why a design system, and why not just write CSS¶
A design system is a short list of named decisions, plus something that stops anyone adding to the list by accident.
That is the whole idea. The rest of this chapter is about why the second half matters more than the first.
The problem, concretely¶
Here are eight greys taken from one real application's stylesheet. All eight were used as a surface colour. Which one is the card?
eight surface greys from one app
The widest gap between any two of those is under 2% of the way from black to white. On a phone in sunlight they are one colour.
Nobody chose eight. They arrived one at a time, and every arrival was reasonable:
- A designer picked
#f5f5f5in one mock-up and#f4f4f5in another, three weeks apart. - A developer typed
#f6f6f7from memory. - A component library was installed and it shipped
#fafafa. - A Figma plugin exported
#f7f7f8because it rounded differently. - Somebody fixed a bug by nudging one value and left the other four alone.
None of that is carelessness. It is the predictable result of a system where there is no definition of correct, so there is nothing for anyone to be wrong about.
What "token" means¶
A design token is a named decision. In CSS it looks like a custom property — a variable you declare once and reference everywhere:
:root {
--neutral-3: #eaeaef; /* the declaration */
}
.card {
background: var(--neutral-3); /* the reference */
}
The important part is not the syntax. It is that the name carries the role, not the
appearance. --neutral-3 does not mean "that light grey". It means the surface a
component rests on. Anything that is a component surface uses it, and the moment the value
changes, everything that is a component surface changes with it.
Compare the two names you could have chosen:
| Name | What a reader learns | What happens when it changes |
|---|---|---|
--grey-100 |
it is grey, and lighter than --grey-200 |
you must find every use and decide, one by one |
--neutral-3 |
it is the resting surface of a component | every resting surface follows, correctly, at once |
--grey-100 is a value with a label on it. --neutral-3 is a decision. Only the second one
can be checked, because only the second one makes a claim.
Why not just write CSS¶
You can, and for one page by one person you should. A design system is overhead and the overhead only pays back at a certain size.
Three things break as you cross that size, and they break in this order.
1. No name means no agreement. Two people cannot pick the same grey. They are not looking at the same screen, in the same light, on the same day.
2. No definition means no test. Suppose you do agree on #f5f5f5. Six months later
someone asks whether the grey text on it is legible. There is no answer, because nobody
wrote down what "legible" was supposed to mean. So the question gets settled by whoever
squints hardest.
3. No test means drift is invisible. This is the one that actually produces the eight greys. A hand-tuned scale is unverifiable by construction: nothing fails when a step moves. The system does not degrade with a bang. It degrades one plausible commit at a time, and every one of those commits looks fine in review.
What breaks first: dark mode¶
The clearest demonstration that a hard-coded value is not a decision is to look at it in a second context. Below, the same five values on a light panel and on a dark one.
hand-picked values, in two contexts
forced light
Storage
41.2 GB of 50 GB used
forced dark — the identical five values
Storage
41.2 GB of 50 GB used
the same card, built from tokens
forced light
Storage
41.2 GB of 50 GB used
forced dark — the identical two token names
Storage
41.2 GB of 50 GB used
Both dark panels are on this page at the same time as the light ones, and no token is redeclared anywhere. The lower pair says --surface and --neutral-11; the upper pair says #ffffff and #999999.
The top-right card is not a styling mistake. It is the correct rendering of the instruction
it was given. #ffffff means "white" and it will keep meaning white for ever, including on
a page where white is wrong.
Also worth noting: that #999999 body text is 2.85:1 against white. Chapter 3 explains
why that number matters and what the legal floor is.
What a design system is not¶
- Not a component library. A library of buttons is one way to deliver the decisions. It is not the decisions.
- Not a Figma file. A Figma file is one place the decisions are used. If it is the only place they exist, the code has its own set.
- Not a brand guideline. A guideline says what to do. A system makes the wrong thing harder to type than the right thing.
The test is simple: if two people can each do a reasonable thing and get different pixels, the system is not doing its job yet.
The four parts¶
Almost every design system, whatever it is called, is these four things:
| Part | What it is | Where nilam keeps it |
|---|---|---|
| Tokens | the named decisions, mostly colour | nilam.tokens.css |
| Scales | the non-colour numbers — type, space, radius, motion | nilam.scale.css |
| Components | the decisions assembled into buttons, cards, dialogs | nilam.components.css |
| Proof | the thing that fails when a decision drifts | src/prove.mjs and the test suites |
The fourth one is the part most systems skip, and it is the reason the other three rot.
How nilam handles it¶
nilam ships 81 colour tokens: six families — neutral, brand, danger, warn, ok,
info — of twelve steps each, plus an ink token per family and one card surface. Every step
is a role, not a shade.
The difference from a hand-tuned system is that each role is a contract, and the value is
computed from it. --neutral-11 is not a grey somebody liked. It is the lightness at which
text reaches 4.5:1 against --neutral-3, found by arithmetic. So the value cannot exist at a
number that breaks the promise, because the promise is what produced the number.
That gives you the thing the eight greys were missing: something fails. More than 7,400 assertions run on every push, plus 31 rendered screenshots compared pixel by pixel. When a step drifts, the build stops.
What nilam does not claim: that using it makes an interface accessible. It makes the colour decisions checkable. Layout, wording, focus order and screen-reader behaviour are still yours, and Honest limits lists exactly where the guarantees end.
Next: why a hex code is a trap — and why #808080 is not half as
bright as white.
Reference: Tokens has every value and every swatch. Why solved colour is the full argument, including the four times the solver was wrong.