When our second enterprise customer asked for their own colors, fonts, and radii, we had two options: fork the CSS or make theming a first-class feature. We chose the hard one. Six months later, one stylesheet powers three completely different brands — and adding a fourth takes an afternoon.
This post walks through the decisions that made it work: the token architecture, the mistakes we made with dark mode, and the tooling that keeps designers and engineers honest.
Tokens are the API
The core idea is boring on purpose: components never reference colors directly. Every visual decision goes through a named token, and themes are just different answers to the same set of questions.
“A theme isn’t a stylesheet. It’s a set of answers to questions your components already know how to ask.”
In practice that means the button doesn’t know it’s blue. It knows it’s brand-colored, and the theme decides what brand means:
[data-theme="default"] { --accents-brand: 226 87% 54%; }
[data-theme="editorial"] { --accents-brand: 20 84% 46%; }
[data-theme="rounded"] { --accents-brand: 262 74% 58%; }
.btn-primary { background: hsl(var(--accents-brand)); }
What we got wrong
Our first dark mode inverted the grey scale mechanically — and every card disappeared into the page. The fix was a rule we now apply everywhere: in light mode the page is grey and cards are white; in dark mode the page is the darkest surface and cards sit a few points lighter. Hierarchy, not inversion.
The second mistake was letting components own spacing. Once two brands wanted different densities, every hardcoded margin became a bug. Density became a token too.
Results
Onboarding a new brand went from a quarter-long project to a config file. The design team maintains themes in Figma variables, CI checks contrast on every token change, and the support tickets about “inconsistent buttons” simply stopped.