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