Skip to main content
design systems

Five Themes, One Pipeline

Brutalist mode and high-contrast mode have opposite goals and ship from the same token source. The layered architecture that makes multi-theme a naming problem, not a CSS-forking problem.


7 min read

Brutalist mode is loud on purpose. High-contrast mode is legible above all else. They want opposite things — and they ship from the same token source, on this site, right now. So does dark mode, and a green-on-black terminal theme.

That isn't a CSS achievement. It's an architecture one, and the whole trick is refusing to let any component know what a hex value is. Here's how the pipeline works — and you can inspect its live output on the design system page, where the token tables are parsed from the real build, not typed by hand.


A Theme Is Not a Stylesheet

The instinct when someone says "we need a dark mode" is to write dark-mode CSS. Then someone wants high contrast, and you write more. Then a marketing push wants a bold aesthetic theme, and now you have four stylesheets drifting out of sync, each one a place a future change has to remember to visit.

The reframe that fixes it: a theme is a set of value assignments, not a set of styles. If your components are written against roles — "surface background," "primary text," "focus ring" — then a theme is just a different answer to "what color is the surface?" No component changes. No stylesheet forks. The theme is data.

That only works if there's a layer between raw values and components where the roles live. Which is the actual architecture.

Raw → Alias → CSS → Components

Four layers, and tokens only ever flow one direction.

Raw scales are the palette of what can exist. On this site that's a dozen JSON files — color ramps, a spacing scale, a type ramp, durations, easings. color.blue.600 is #2563eb here and nowhere else. Raw tokens have no opinion about usage; they're just the vocabulary.

Semantic aliases are where meaning gets assigned. This is the load-bearing layer. One file per functional theme maps raw values onto roles:

// alias/light.json — the light theme, as role assignments
{ "t": { "color": {
  "bg":     { "surface": { "value": "#ffffff" } },
  "brand":  { "value": "#2563eb" },
  "border": { "focus":  { "value": "#2563eb" } }
}}}

alias/dark.json assigns different values to the same roles. That file — not a stylesheet — is the dark theme. alias/hc.json is the high-contrast theme. Each one is a few dozen decisions about what a role resolves to.

Generated CSS is what the browser receives. Style Dictionary — Amazon's open-source token compiler — flattens the JSON into custom properties:

:root                    { --t-color-bg-surface: #ffffff; }
[data-theme="dark"]      { --t-color-bg-surface: #0a0a0f; }
[data-theme="hc"]        { --t-color-bg-surface: #000000; }

Components only ever read the aliases:

.card {
  background: var(--t-color-bg-surface);
  border: 1px solid var(--t-color-border-default);
}

The card has no idea five themes exist. Switch data-theme on <html> and every custom property re-resolves. That's the whole mechanism. The card that shipped before the terminal theme existed rendered correctly in it the moment it was added, because it was never styling colors — it was styling roles.

What Doesn't Change Is the Point

Here's the part the token tables make obvious once you see them side by side: most tokens aren't themed at all.

The spacing scale is identical in every theme. So is the type ramp, the z-index scale. A theme overrides color and — for the aesthetic themes — a few structural tokens. Dark mode is roughly forty color reassignments and nothing else. It cannot move a layout, because layout tokens aren't in its file.

That constraint is a feature. It means a theme can never break the grid, never shift a component's spacing, never introduce a bug that only appears in one skin. The blast radius of "add a theme" is bounded to color and, at most, a handful of deliberately-exposed structural knobs.

Which is exactly what makes the aesthetic themes safe to be weird:

  • Brutalist zeroes every radius and sets every motion duration to 0ms. It's confrontational by design — but it's still the same components, just with different values in the corners-and-timing tokens.
  • Terminal goes green-on-black and monospaces everything. A costume, not a rebuild.

Both are additional alias files. That's it. Adding "terminal" was an evening, not a project, because the system did all the structural work in advance.

The Unglamorous Half

Architecture gets the headline; the details are what make it survive contact with a real browser.

Functional themes inline; aesthetic themes lazy-load. Light, dark, and high-contrast are compiled into the core CSS because most visitors use one of them and they should cost zero extra requests. Brutalist and terminal compile to separate files under /public/themes/ and load only when someone actually picks them — you don't pay for the costume you never wear.

First paint can't flash the wrong theme. A tiny script sets data-theme before the first paint, and the generated CSS carries a prefers-color-scheme fallback so that even with no JavaScript and no stored preference, the OS setting wins with passing contrast. (That fallback also closed a real accessibility bug — hydration was dropping the attribute, and the audit caught it because contrast math changes per theme.)

Switching is one transition, not a repaint storm. Changing 90-plus custom properties at once runs through the View Transitions API, so it reads as a single calm crossfade instead of the page tearing through intermediate states.

The Governance Line

The architecture only holds if one rule holds: a component may never hardcode a value. The moment one does, it works in the current theme and breaks silently in the other four — and it teaches the next person in code review that hardcoding is fine here.

So the rule is absolute, which is what makes the exceptions visible. A design system doesn't die of bad token structure. It dies of the Tuesday a deadline made a hardcoded hex feel reasonable. Naming decisions are the durable part; values are the disposable part; and keeping those two straight is most of the job.


See the live token tables, the layer diagram, and the failure modes this system guards against on the design system page. The same token-first thinking runs through the ReactZero component libraries.