Skip to content

How to Run Multi-Brand Theming in One Design System

Run several brands off one component library with layered design tokens and CSS custom properties, no forked components and no duplicated code per brand.

· · 7 min read
Colorful sheets of paper in red, yellow, green, and blue stacked together

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.

Photo by Andy Brown on Unsplash
The same moulded chair repeated in white and in orange
Photo by Clay Banks on Unsplash

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

LayerExampleBrand-aware?
Primitivesblue-500, space-4No
Semantic tokenscolor.action.primary, color.surface.dangerNo, identical names across brands
Brand tokensPer-brand override of what color.action.primary resolves toYes

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.

Photo by Christina Rumpf on Unsplash

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.

Frequently Asked Questions

Do I need a separate component library per brand?
No, and if you're maintaining one, that's the exact problem this architecture fixes. A single component library should never know which brand it's rendering. Components consume semantic tokens (color.action.primary, radius.button) and never reference a brand name directly. The brand identity lives entirely in which token file is loaded, not in the component code. I've seen teams maintain three near-identical Button components differing only in a hex value, that duplication is a sign the token layer is missing, not that multi-brand needs more components.
How many token layers does a multi-brand system actually need?
Three, and adding a fourth rarely earns its complexity. Primitive tokens are your raw palette (blue-500, space-4), unaware of any brand. Semantic tokens map meaning to those primitives (color.action.primary, color.surface.danger) and stay identical across brands. Brand tokens override the semantic layer per brand, redefining what color.action.primary actually points to. Components only ever consume semantic tokens. That three-layer split is what lets you add a fourth brand without touching a single component file.
Can multi-brand theming work with CSS custom properties alone, no build tool?
Yes, for a small number of brands, plain CSS custom properties scoped to a brand class or data attribute work fine and add zero build complexity. Define your semantic custom properties once, then override them inside .brand-acme or [data-brand='acme'] selector blocks. Runtime brand switching becomes a single class or attribute change. Where a build tool like Style Dictionary or Panda CSS starts earning its keep is past roughly four or five brands, or once you need to generate the same tokens for iOS and Android alongside web, at that point hand-maintaining the CSS gets error-prone fast.
What's the most common mistake teams make going multi-brand?
Skipping the semantic layer and letting components reference brand tokens directly. It works for brand one. Adding brand two means either forking the component or scattering conditional brand checks through the codebase, both of which defeat the entire point of a shared design system. The fix costs almost nothing upfront: always insert a semantic token between the primitive palette and the component, even if you only have one brand today. Retrofitting that layer onto fifty existing components later is a multi-week project I would not wish on anyone.