Architect a theming system that supports multiple brands from a single component codebase using CSS custom properties, semantic token layers, and theme injection strategies.
Architect a theming system that supports multiple brands from a single component codebase using CSS custom properties, semantic token layers, and theme injection strategies.
Multi-brand systems require a precise token hierarchy to avoid token soup. Tier 1 Global tokens contain raw values and are never used directly in components — examples include --global-color-blue-500: #3B82F6 and --global-spacing-4: 1rem. Tier 2 Semantic tokens reference global tokens and carry intent — examples include --color-interactive-primary: var(--global-color-blue-500) and --space-component-gap: var(--global-spacing-4). Tier 3 Component tokens reference semantic tokens and are scoped to a single component — examples include --button-background: var(--color-interactive-primary) and --button-radius: var(--radius-component). Brand theming overrides only Tier 2, substituting brand-specific values for semantic tokens without touching any component. This isolates brand changes to a single layer, meaning components are completely brand-agnostic and require zero edits when a new brand is added to the system.
The most performant theming approach requires zero JavaScript for a theme switch — CSS handles it entirely via attribute selectors. Apply a data-theme attribute to the root element and define token overrides per brand:
1/* Base tokens — Brand A */
2:root {
3 --color-interactive-primary: #3B82F6;
4 --color-surface-elevated: #ffffff;
5 --color-text-default: #111827;
6 --radius-component: 6px;
7 --font-family-body: 'Inter', sans-serif;
8}
9
10/* Brand B override */
11[data-theme='brand-b'] {
12 --color-interactive-primary: #7C3AED;
13 --color-surface-elevated: #f5f3ff;
14 --color-text-default: #1e1b4b;
15 --radius-component: 2px;
16 --font-family-body: 'IBM Plex Sans', sans-serif;
17}
18
19/* Brand C override */
20[data-theme='brand-c'] {
21 --color-interactive-primary: #059669;
22 --color-surface-elevated: #ecfdf5;
23 --color-text-default: #064e3b;
24 --radius-component: 16px;
25}
26
27/* Component: works for all brands with zero changes */
28.button-primary {
29 background: var(--color-interactive-primary);
30 border-radius: var(--radius-component);
31 color: var(--color-surface-elevated);
32 font-family: var(--font-family-body);
33}Wrap the CSS attribute approach with a typed React provider to get autocomplete and compile-time safety:
1type Brand = 'brand-a' | 'brand-b' | 'brand-c';
2type ColorScheme = 'light' | 'dark';
3
4interface ThemeProviderProps {
5 brand: Brand;
6 colorScheme?: ColorScheme;
7 children: React.ReactNode;
8}
9
10export function ThemeProvider({
11 brand,
12 colorScheme = 'light',
13 children,
14}: ThemeProviderProps) {
15 return (
16 <div
17 data-theme={brand}
18 data-color-scheme={colorScheme}
19 style={{ display: 'contents' }}
20 >
21 {children}
22 </div>
23 );
24}
25
26// Usage in the application shell
27export function App() {
28 return (
29 <ThemeProvider brand="brand-b" colorScheme="dark">
30 <Header />
31 <main><Outlet /></main>
32 </ThemeProvider>
33 );
34}The style={{ display: 'contents' }} trick prevents the wrapper div from participating in flex or grid layouts, avoiding layout regressions. Components consume themes via CSS custom properties only — no useContext for visual properties, which would force component re-renders on every theme change.
Automate the Figma-to-code pipeline so designers can update tokens without engineering intervention. Export tokens from Figma using the Tokens Studio plugin, then use Style Dictionary to transform JSON to per-brand CSS custom property files:
1// style-dictionary.config.mjs
2import StyleDictionary from 'style-dictionary';
3
4const brands = ['brand-a', 'brand-b', 'brand-c'];
5
6// Build global tokens
7StyleDictionary.extend({
8 source: ['tokens/global.json'],
9 platforms: {
10 css: {
11 transformGroup: 'css',
12 prefix: 'global',
13 buildPath: 'packages/tokens/dist/',
14 files: [{
15 destination: 'global.css',
16 format: 'css/variables',
17 options: { selector: ':root' },
18 }],
19 },
20 },
21}).buildAllPlatforms();
22
23// Build per-brand semantic overrides
24for (const brand of brands) {
25 StyleDictionary.extend({
26 source: [`tokens/${brand}/semantic.json`],
27 platforms: {
28 css: {
29 buildPath: 'packages/tokens/dist/themes/',
30 files: [{
31 destination: `${brand}.css`,
32 format: 'css/variables',
33 options: { selector: `[data-theme="${brand}"]` },
34 }],
35 },
36 },
37 }).buildAllPlatforms();
38}Run this build in CI on every token JSON change to keep CSS files in sync with Figma automatically.
Marketplace products often need a sub-brand inside a parent brand — each merchant customizes their storefront within the marketplace shell. CSS custom properties cascade naturally, enabling nested theme inheritance where inner data-theme blocks override only the tokens they define and unset tokens inherit from the nearest parent theme:
1/* Marketplace parent sets blue as interactive primary */
2[data-theme='marketplace'] {
3 --color-interactive-primary: #3B82F6;
4 --color-surface-page: #f9fafb;
5 --font-family-body: 'Inter', sans-serif;
6}
7
8/* Merchant overrides only their accent color */
9/* Inherits --color-surface-page and --font-family-body from marketplace */
10[data-theme='merchant-123'] {
11 --color-interactive-primary: #dc2626;
12}1function MarketplacePage({ merchantId }: { merchantId: string }) {
2 return (
3 <ThemeProvider brand="marketplace">
4 <GlobalHeader /> {/* uses marketplace blue */}
5 <ThemeProvider brand={`merchant-${merchantId}` as Brand}>
6 <MerchantStorefront /> {/* uses merchant red, inherits page bg */}
7 </ThemeProvider>
8 </ThemeProvider>
9 );
10}Limit merchant-configurable tokens to a documented subset — avoid exposing all 200 tokens to merchants. Publish a MerchantThemeProvider that only accepts the sanctioned 10-15 override properties to prevent brand system violations.
Refactor your design tokens into the three-tier hierarchy. Create two brand theme files and test switching via data-theme attribute in a Storybook theme switcher addon built with globalTypes.
Your progress is saved in this browser, on this device only.