A client asked me to add a second brand to their existing product last year. Same app, same features, different name and colors for a white-label partner. The estimate came back at six weeks, because every component had brand-specific hex values hardcoded directly into the JSX. Six weeks to change some colors.
That's what happens when a design system was never built to be brand-agnostic. The fix isn't more components. It's a layer of indirection between the raw palette and the thing a component actually asks for.
TL;DR: Multi-brand theming works when components only ever reference semantic tokens (color.action.primary), never brand-specific values directly. Layer primitives, then semantic tokens, then brand overrides, and adding brand four costs a token file, not a component fork.
Why Do Multi-Brand Systems Usually Break First?
They break because someone let a component reach past the semantic layer straight to a brand's raw color. background: var(--acme-blue) inside a Button component ties that component to one brand forever. The moment a second brand needs a different blue, you're either forking the Button or sprinkling if (brand === 'nova') checks through the render logic.
The fix is a strict rule: components consume semantic tokens only, never primitives, never brand names. color.action.primary means the same thing to every component regardless of which brand is active; what changes is which hex that semantic token resolves to, and that resolution happens one layer up, not inside the component.
If you haven't built the underlying library yet, building a design system in Figma covers the component and variable groundwork this assumes. And the mechanism here is the same one that drives light and dark themes, so implementing dark mode properly is a useful smaller-scale rehearsal before you take on brand four.
What Does the Three-Layer Token Structure Actually Look Like?
Three layers, and I'd resist adding a fourth without a very specific reason:
- Primitives: the raw palette.
blue-500: #2563eb,space-4: 16px. No brand awareness at all. - Semantic tokens: meaning mapped to primitives.
color.action.primary: {blue-500},color.surface.danger: {red-500}. Identical names across every brand. - Brand tokens: per-brand overrides of the semantic layer. Brand Acme points
color.action.primaryat its own blue; Brand Nova points the same semantic name at a completely different hue.
Panda CSS's approach to this is worth studying directly: each brand compiles to a scoped selector, and "switching brands is a single attribute change" with token mismatches caught as type errors rather than silent production bugs (Panda CSS, 2026). You don't need Panda specifically to get this benefit, the architecture pattern is the point, not the tool.
| Layer | Example | Brand-aware? |
|---|---|---|
| Primitives | blue-500, space-4 | No |
| Semantic tokens | color.action.primary, color.surface.danger | No, identical names across brands |
| Brand tokens | Per-brand override of what color.action.primary resolves to | Yes |
How Do You Wire This Up With Plain CSS?
For two to four brands, custom properties alone handle this without any build step:
:root {
--color-action-primary: var(--blue-500);
--color-surface-danger: var(--red-500);
}
[data-brand="nova"] {
--color-action-primary: var(--nova-teal-600);
}
[data-brand="acme"] {
--color-action-primary: var(--acme-orange-500);
}
.button-primary {
background: var(--color-action-primary);
}
Switching brands at runtime is a single data-brand attribute change on the root element, no page reload, no component re-render logic required. The .button-primary class never mentions a brand name, exactly the separation that keeps the component library brand-agnostic.
One detail people miss: nest your data-brand scoping high enough that a single page can render two brands side by side, useful for marketplace or partner-preview contexts where a user compares their storefront against a template. Because the override lives on an ancestor attribute rather than baked into the component tree, two [data-brand] blocks can sit next to each other in the same DOM without conflict.
When Do You Actually Need a Build Tool Instead?
Past four or five brands, or the moment you need the same tokens on iOS and Android alongside web, hand-maintaining CSS custom property overrides starts getting error-prone. That's where a tool like Style Dictionary earns its place: one token source, transformed into whatever format each platform and brand combination needs, generated instead of hand-typed. We cover the Figma-to-code side of that pipeline in our design tokens and Style Dictionary guide.
The web layer of this is really CSS custom properties doing the heavy lifting either way, whether you write the overrides by hand or a build tool generates them for you. Same mechanism, different amount of hand-typing.
What Should You Check Before Adding Brand Two?
Before a second brand ever gets scoped into a sprint, run this check: grep your components for any hex value, brand name string, or conditional brand check. Any hit is a component that will need surgery, not just a new token file, when brand two arrives. Fixing that now, while there's still only one brand to test against, costs a fraction of what it costs after three teams have shipped features against the broken pattern.
One more check worth doing per brand: push each brand's text and surface pairs through the WCAG contrast checker. A palette swap that reads fine for brand one can quietly drop a secondary label below 4.5:1 for brand two.
A Worked Example: Adding Brand Three
Here's what the fourth brand actually cost on that same client project once the semantic layer existed. Their token file per brand held 38 semantic overrides: 22 color values, 6 radius values, 4 shadow definitions, and 6 typography assignments. Writing it took an afternoon, most of which was contrast checking rather than typing. Zero component files changed. The compiled CSS grew by about 3KB before gzip, which is nothing next to the 140KB of component JavaScript already shipping.
Compare that against the original hardcoded approach: 61 components contained at least one literal hex value, and 14 of those had brand conditionals in the render path. At an honest hour per component including review and regression testing, that's the six-week figure the first estimate landed on. The token layer didn't make the work faster. It made most of the work stop existing.
Where Multi-Brand Theming Actually Breaks
Logos and illustrations, almost every time. Colors and spacing map cleanly onto tokens; a wordmark with a different aspect ratio does not. Brand Acme's logo is 4:1 and Brand Nova's is 1:1, and now your header component needs a genuine layout variation rather than a token swap. I handle that with a slot rather than a conditional, but it's the one place where pure token theming genuinely runs out.
The second break is copy. Brands rarely want identical wording, and once a component hardcodes a label, you're back to forking. Push user-facing strings into the same override mechanism as your color tokens and the problem mostly disappears.
Third, and this is where I'll be blunt: brand-specific density. A finance brand wants tighter rows than a consumer brand, and teams try to solve it by redefining space-4 from 16px to 12px per brand. Don't. Primitives should stay fixed across brands, or a spacing value means something different depending on which brand loaded, and every visual regression test you own becomes untrustworthy. Define a separate semantic density scale instead.
The six-week estimate I mentioned at the start became four days once we inserted the semantic layer properly. Same components, same features, just one extra layer of indirection between the palette and the button.