Skip to content
Back to Design Hub

Multi-Brand & White-Label Theming

Token layering, brand asset pipelines, and runtime theme switching for multi-tenant products.

Design SystemsAdvanced

Why

Hardcoded brand colors mean a new tenant takes a fork and a 3-month release. Token-layered theming lets you onboard a white-label brand in hours by swapping one token file, with zero code branches.

According to the 2024 Spark Design Systems survey, 78% of organizations with a design system report faster product delivery. JavaScript is used by 62% of developers per the Stack Overflow 2024 survey and component libraries that align with design tokens cut handoff time by 60%.

When

Build multi-brand theming when you have 2+ brands or any white-label/agency use case. Don't build it speculatively for 'maybe one day' — the abstraction tax isn't worth it.

How

1. Split tokens into layers — primitive (blue-500), semantic (action-primary), brand (logo, font) 2. Brand package — each brand ships a tokens.{brand}.json + assets/ (logo, fonts) 3. Build pipeline — Style Dictionary emits one CSS var set per brand to /dist/brands/{brand}.css 4. Runtime switch — load the brand CSS file on <html data-brand="acme">, vars cascade 5. Component code — references var(--color-action-primary), never brand-specific values 6. Asset pipeline — brand logos via next/image with brand-scoped public/brands/{brand}/ 7. Test matrix — Storybook renders every component under each brand in CI

Example

// tokens/acme.json — brand override { "color.action.primary": "{color.blue.600}", "font.family.body": "AcmeSans, sans-serif", "asset.logo": "/brands/acme/logo.svg" } // ThemeSwitcher.tsx — runtime brand load function ThemeSwitcher({ brand }: { brand: "acme" | "globex" }) { useEffect(() => { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = `/brands/${brand}.css`; document.head.appendChild(link); document.documentElement.dataset.brand = brand; return () => link.remove(); }, [brand]); return null; } /* brands/acme.css — emitted by Style Dictionary */ :root[data-brand="acme"] { --color-action-primary: #2563eb; --font-family-body: "AcmeSans", sans-serif; }

Common Problems

  • ⚠Brand-specific values leak into components — a Button hardcodes Acme blue, Globex gets blue buttons
  • ⚠Loading all brand CSS upfront — 10 brands × 50KB = 500KB wasted on the client
  • ⚠No brand test matrix — Acme looks right but Globex has broken contrast, found in production

Best Practices

  • ✓Components reference only semantic tokens — never primitive or brand-specific values
  • ✓Lazy-load brand CSS — inject the one stylesheet for the active tenant at runtime
  • ✓Render a brand matrix in Storybook — every component × every brand in CI chromatic snapshots

FAQ