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.
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.
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.
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.