Skip to main content

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.

Design systemsTokensThemingTooling
9 Runtime states3 palettes by 3 type pairings, persisted per visitor
37 · 59 · 30 Tokens by tierprimitive, semantic, component, counted from index.css
437 Token reads in componentsvar() calls outside the shadcn folder
1 Known leaka component token that reads a primitive, flagged in the file
Live on this site gysler-designs.ch, March to September 2026. Figures counted from the token file and the component folder on 12 October 2026. Everything on this page is drawn from the live tokens, so it changes with the theme widget.
37 Primitives paper, ink, accent, confidence and object ramps
59 Semantic tokens text, surface, edge, signal, state, confidence, type, space
30 Component tokens label, chip, button, card, input, nav, band, rule
37 / 15 Tokens a palette rewrites Ink / Warm, semantic tier only
3 Lint rules tier-1 reads, literal fonts, hex colours
44 Commits to the token file 19 Mar to 9 Sep 2026

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

  1. 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.
  2. 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.
  3. 03A lint rule, not a convention. npm run lint:tokens fails 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.

Nine runtime states, one site Live: a cell sets the theme
Studio
Ink
Warm
Finlandica · Spline
Tuffy · Spline
Spectral · Inter
Three palettes across, three type pairings down. The cell with the ring is the state you are in now. Every value on this page re-resolves when you pick another, which is the whole argument: nothing here is hardcoded.

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.

One value, three names Computed live

Ink to a filled button

Tier 1 · ink, darkest--c-ink-900 rgb(20, 20, 15)
Tier 2 · the inverse surface--surface-inverse rgb(20, 20, 15)
Tier 3 · button, filled--btn-primary-bg rgb(20, 20, 15)

Paper to the text on it

Tier 1 · paper, page--c-paper-100 rgb(252, 252, 251)
Tier 2 · text on inverse--text-inverse rgb(252, 252, 251)
Tier 3 · button, filled, label--btn-primary-text rgb(252, 252, 251)

What the visitor sees

Read the caseOutcome bandSelected chip

All three read the inverse surface and the inverse text. Switch the palette and they move together, because the semantic tier moved once.

In the Studio palette the three swatches in a chain are identical, and the computed value beside each says so. Switch to Ink or Warm and the primitive stays put while the two names below it move together: a theme rewrites the middle tier only, and the button on the right never hears about it.

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.

Thirty component tokens, sixteen reads Hover a chip
LabelChipButtonCardInputNavBandRule
--text-primary5 --chip-text-hover--chip-edge-hover--btn-secondary-text--input-text--nav-text-active
--surface-inverse3 --chip-selected-bg--btn-primary-bg--band-bg
--text-inverse3 --chip-selected-text--btn-primary-text--band-text
--edge-hairline3 --card-edge--input-edge--rule
--edge-strong3 --chip-edge--btn-secondary-edge--card-edge-hover
--surface-raised2 --card-bg--input-bg
--text-secondary2 --label-text--nav-text
--text-body1 --chip-text
--text-tertiary1 --input-placeholder
--surface-recessed1 --chip-soft-bg
--hover-wash1 --btn-secondary-bg-hover
--focus-ring1 --input-edge-focus
--radius-sm1 --btn-radius
--radius-md1 --card-radius
--space-inset1 --card-inset
--c-ink-8001 --btn-primary-bg-hover Tier 3 reading tier 1. The ink ramp has no hover semantic yet; the comment in the file flags it for the next phase.
Rows are the semantic tokens that components depend on, most reused first. The count is how many component tokens read that one role. Hover a component's name in the legend, or any chip, to see that component's whole footprint across the tiers.

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.

Three rules the build runs
Fails onWhat the script greps forNamed exemptions
Tier-1 read in a componentvar(--c-paper | ink | accent | conf | goldnone; the object palette --c-obj-* is allowed by name
Literal font familyfont-family: '…' or fontFamily: '…'3 product mockups, which are screenshots-as-code
Literal hex colour#rgb or #rrggbb in a .jsx file8 illustrations and animations, the theme widget, the chart primitive
FailsPasses
color: '#2450c8'color: 'var(--text-signal)'fontFamily: 'Spline Sans'fontFamily: 'var(--f-body)'background: var(--c-paper-200)background: var(--hover-wash)
The script is eleven lines of grep in scripts/lint-tokens.sh and runs in npm run lint. Each exemption is a file name on one line, so the list of places where literal colour is the design is itself documentation.

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

Let the visitor change the theme, nine combinationsToken discipline became a correctness requirement, and every hardcoded value became a visible bug
Three tiers, components read tiers 2 and 3 onlyA palette rewrites 37 semantic tokens and 437 component reads follow without an edit
Keep tier 3 thin and reference tier 2 onlyThirty component tokens, one known dip to a primitive, flagged where it lives
Lock the layer with a script, with named exemptionsIllustrations and mockups keep their literal colour; everything else fails the build
Make the documentation read the live tokensThe System page and this case show the system as it ships, in whichever theme is loaded
Next case A correction surface for an 85% accurate pipeline Scandit · 2023 to 2024 · Sole IC, research through synthesis