Sriram Kansala › A multi-brand token system

A multi-brand token system

157 colors to 64, every pair checked for AA · 2025

TL;DR

I proposed this rebuild and was its only designer, with one or two engineers. My lead signed off. I cut the color tokens from 157 to 64, in three tiers. The platform is B2B SaaS that sells one interface to multiple clients, each in its own brand.

I designed the model, the naming grammar, the accessibility pairing rules and the migration plan. The contrast-validation pipeline was built with the engineers. Every token pairing is validated for WCAG AA when it is defined, before any screen uses it.

By my estimate, theming a new client now takes under an hour, down from two or three days of remapping by hand. One product team builds from the system, and its naming grammar has since spread beyond color.

It rolled out over three months in 2025, alongside production work.

Four names for one color

The system had grown one token per component, so the color layer read like a directory of the component library:

Those four names carry one value. That was the growth model: every new component minted its own tokens, so the count rose one for one with the component library. Theming paid for it. Onboarding a client meant days of remapping by hand, changing the same color everywhere it had been declared again. And nothing stopped a bad contrast pairing, so accessibility was whatever QA happened to catch, late or not at all.

Names that state a job

Before designing my own, I studied how four mature systems had solved this: Salesforce Lightning, Shopify Polaris, Material and Adobe Spectrum. The idea I leaned on hardest came from my reading of Lightning: validate contrast when a semantic token is defined, before any screen ships. Material keeps brand identity apart from interaction patterns, so a palette swap never touches component logic. Spectrum writes interaction states into the token name. All four put a layer between raw values and the components that use them.

So I set one structural rule. Every token is named for the job it does, such as Text color/primary.

That rule made theming a swap of primitives and tied accessibility to pairings validated up front. It also ended the memorizing, because picking a color now means naming its job.

How hard to constrain it

I limited the colorable surface to four elements: text, background, border and icon. Mirroring every component would have re-created the sprawl I was removing. Four elements kept 64 tokens legible and themable. That left out chart colors, which have no purpose to name, so they became a documented exception, covered under Rollout.

Three tiers, one direction of dependency

Primitives are raw values with nothing attached: Primary/500 is a hex and nothing more. They are the only layer that changes between brands.

Semantic tokens sit above them and name the job on a fixed grammar, {element}/{role}, with a numbered modifier where a role has steps (accent-1, accent-2). Text color carries the type hierarchy: primary, secondary, tertiary, disabled and inverse. Icons mirror it. Background covers surface depth in numbered steps, 0 to 5. Border handles structure the same way, and both have accent and status variants.

Components consume semantic tokens only and never touch a raw value. A button references Background/accent-1 and Text color/inverse. The colors those resolve to depend on which primitive set is loaded, so the component code never changes when the brand does.

Contrast checked at definition time

The old model designed first, audited later and shipped whatever the audit missed. The new one moves the check to definition time. Every token pairing is validated for WCAG AA when it is defined. A screen that sets Text color/primary on Background/1 gets that pair’s checked 14.43:1 ratio, and nobody has to measure it again.

Validation covers the pairings the semantic layer defines, a set the four-element constraint keeps small enough to check exhaustively. No client brand themed on it has failed AA so far. The demo on this page runs a working model of the system with two fictional brands. It computes every contrast ratio live, and a build check fails the site’s build if a token rule breaks. I designed it and built it by directing Claude Code.

A new brand in under an hour (estimated)

The client supplies a brand palette. A script maps it onto the primitive layer, validation checks every pairing for AA, and it deploys. I estimate that takes under an hour. Before, it took two or three days, because each color had to be remapped by hand everywhere it was declared.

Swapping the foundation under a shipping product

I rolled the new layer out over three months, alongside production work. The old tokens kept shipping the whole time, because pausing the product to rebuild its color layer was never an option.

The hardest pushback came from engineers, over what the migration would cost them.

Migration put education before enforcement. The renames broke every habit people had for picking a color, so documentation and walkthroughs came first. Then the old tokens began to emit deprecation warnings, and later the warnings became errors. Components moved over gradually instead of breaking at once.

A gate on growth

The original failure was unbounded growth, so I put a gate on it. A new token request needs a use case, a reason no existing token covers it, and a proposed name. The default answer is reuse. No one has filed a request yet, so the gate is still untested.

I never measured whether contrast defects in QA dropped after rollout, so I can’t say that they did.