06 · This website · March to September 2026
Design system, tokens and components, end to end
A design system a visitor can re-theme without breaking it
The site you are reading lets a visitor swap the palette and the type pairing, nine combinations in all. That one feature made a token architecture a correctness requirement, not a tidiness habit. This is how the tokens are built, how they carry into the components, and what keeps a hardcoded value from slipping back in.
The live specimen is the System page on this site, which reads the same tokens at runtime.
The problem
A portfolio for AI product design should show its own system working, not describe one. So the theme widget lets a visitor change the palette (Studio, Ink, Warm) and the type pairing (Finlandica with Spline, Tuffy with Spline, Spectral with Inter). That is nine runtime states. A hex colour or a font name written into a component looks fine in the state the developer happened to be in and wrong in the other eight. Every colour, family, size, space and duration has to resolve through a custom property, and something has to stop the exceptions from accumulating.
My role
Solo, on my own site. I wrote the tokens, the palette and type overrides, the component layer, the lint rule that locks it, and the live System page that reports on it. Forty-four commits to the token file between March and September 2026, inside a site of about five hundred and fifty commits.
Constraints
Three palettes and three pairings that all have to hold on every page, including the long case study pages with their own layout vocabulary. Two illustration systems (the isometric hero, the project card grounds) that need literal colour by design. A shadcn component folder that expects its own variable names. And one person maintaining all of it, so the rules have to be enforced by a script rather than remembered.
Three design bets
- 01Three tiers, and components may only read the top two. Primitives name what a value is. Semantic tokens name what it does. Component tokens exist only where a component departs from the semantic default. A theme rewrites the semantic tier and nothing else.
- 02The system reports on itself. The System page and the artefacts in this case read their values at runtime from the same custom properties as the rest of the site, so they cannot drift from what ships.
- 03A lint rule, not a convention.
npm run lint:tokensfails the build on a tier-1 read, a literal font family or a hex colour in a component, with a short, named list of exemptions.
Act I
Frame the constraint
System · The constraint
Nine states, and a hardcoded value is wrong in eight of them
The theme widget is the hardest constraint in the system, and it was chosen on purpose. Three palettes by three type pairings is nine combinations a visitor can land in, persisted in their browser, so there is no single state to design for. A literal #14140f in a component reads as ink on the Studio palette and vanishes on Ink. A literal font stack ignores the pairing the visitor chose.
The grid below is live. Each cell sets the site to that combination, the same way the theme widget does, so you can see this page and its artefacts re-resolve. Nothing in the cells is an image.
Act II
Build the layers
System · Token architecture
Three tiers, and components may only read the top two
Tier 1 is the raw material: the paper ramp, the ink ramp, one accent ramp, a confidence ramp with its washes, and the object palette for illustration. Nothing reads it directly. Tier 2 names a job: primary text, raised surface, hairline edge, signal. Each points at a primitive, and this is the only tier a palette rewrites. Tier 3 is kept thin on purpose: a component token exists only where a component departs from the semantic default or needs a state the tier above has not named, and it references tier 2 only.
The chain below follows one value down three names. The swatches are filled by the live custom properties. In the default palette all three are identical. Switch to Ink and the primitive stays where it is while the two names below it move together, which is the proof that a theme rewrites the middle tier only.
Ink to a filled button
Paper to the text on it
What the visitor sees
Read the caseOutcome bandSelected chipAll three read the inverse surface and the inverse text. Switch the palette and they move together, because the semantic tier moved once.
System · Carry-over
How a semantic token carries into seven components
Reuse is the point of the middle tier. A component never invents a colour; it names which semantic role it is playing. The button's filled background is the inverse surface, the same token the outcome band and the selected chip use. The card's edge is the hairline, the same token the input's edge and every rule on the site use. When the Ink palette rewrites the inverse surface once, the button, the band and the chip follow without being touched.
The matrix lists every component token in the file against the semantic token it reads. Hover a column to see every component that depends on that one role. The swatch in each cell is the live value.
System · The gate
A rule the build runs, not a rule I remember
Conventions decay. The lock is a shell script in the build: it greps the component folder for a tier-1 read, a literal font family and a hex colour, and fails if it finds one. The exemptions are named in the script, one line each, and they are the places where literal values are the design: the isometric hero and the animations, the product mockups that are screenshots-as-code of other products, the theme widget's own swatches, and shadcn's chart primitive.
The script is short enough to read in a minute, which is the other half of the rule: a lint nobody understands gets disabled.
| Fails on | What the script greps for | Named exemptions |
|---|---|---|
| Tier-1 read in a component | var(--c-paper | ink | accent | conf | gold | none; the object palette --c-obj-* is allowed by name |
| Literal font family | font-family: '…' or fontFamily: '…' | 3 product mockups, which are screenshots-as-code |
| Literal hex colour | #rgb or #rrggbb in a .jsx file | 8 illustrations and animations, the theme widget, the chart primitive |
color: '#2450c8'color: 'var(--text-signal)'fontFamily: 'Spline Sans'fontFamily: 'var(--f-body)'background: var(--c-paper-200)background: var(--hover-wash)Act III
Hold the line
System · What leaked
One dip, one shim, and an exception by design
Honest systems keep a list. The button's hover background reads a primitive directly, because the ink ramp has no 'hover' semantic yet; the comment in the file says so and names the phase that fixes it. The shadcn folder expects its own names, so a nineteen-line compatibility shim aliases the semantic tier for it, and new work does not use the shim. And the object palette is read by the hero and the project cards on purpose, because an illustration palette is not a text palette and should not invert with one.
None of these is hidden in a component. Each lives in the token file, next to the tier it breaks, with a reason. That is the difference between debt and rot.
Outcome
Nine runtime states, zero literal colours in components, and a page that proves it while you read
- 01Three palettes and three type pairings hold across every page, including the long case studies, because the semantic tier is the only thing a theme rewrites.
- 02Thirty component tokens cover seven components by referencing fifty-nine semantic roles, so a change to one role carries to every component that plays it.
- 03A three-rule lint fails the build on a tier-1 read, a literal font or a hex colour in a component, with eleven named exemptions where literal values are the design.
- 04The System page and the artefacts in this case are specimens, not screenshots. They read the live tokens, so the documentation cannot drift from the product.
Decisions to outcomes