Skip to content

Multi-Brand Theming: One Design System, Many Brands

Multi-Brand Theming in a Single 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.

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

A circle of different paint color swatches arranged on a white surface
Photo by Andy Brown 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.

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.

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.

Blue, white, green, and red textile swatches arranged together showing brand color variety
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.

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.