Tailwind CSS 56 🎨 Dynamic Theme Switching and Multi-Brand Design Tokens Architecture
A single-brand application has one set of colors, one font stack, one spacing scale. A multi-brand application has several, and the same component must render in each brand’s identity without being rewritten. Dynamic theme switching and multi-brand design tokens are the two halves of this problem. Theme switching changes the active theme at runtime: light to dark, brand A to brand B. Design tokens define the values that each theme supplies: the color palette, the radius, the font, the shadow. Together they let one component tree render in multiple visual identities, driven by CSS variables and a Tailwind configuration that maps the variables to utility classes. The result is a component that never mentions a specific color, and a theme that can be swapped without a rebuild.
Key point: A design token is a named value, not a color. The component uses bg-primary, not bg-blue-600. The theme defines what primary means. Changing the theme changes every component that uses primary, without editing a single component.
Why theme switching and multi-brand tokens matter
The hardcoded-color problem. A component that writes bg-blue-600 is a component with an opinion about blue. When the brand changes to orange, every instance of bg-blue-600 must be found and replaced. The replacements are scattered across the codebase, and a missed one produces a component that renders in the wrong brand.
The dark-mode problem. A dark theme is not the light theme with the colors inverted. The contrast ratios differ, the shadows behave differently, and the images may need a different treatment. The theme must define its own values, not derive them from the light theme by a formula.
The multi-brand problem. A white-label application ships the same features under several brands. Each brand has its own logo, its own primary color, and its own typography. The components are identical; only the tokens differ. Without a token layer, each brand requires a fork of the CSS.
The runtime problem. A user can switch themes at runtime, and the switch should be instant. If the theme requires a page reload, the switch is slow and the state is lost. CSS variables can be changed at runtime without a reload, which makes the switch instant.
The build problem. A theme that requires a rebuild cannot be changed by a non-developer. A theme that is a CSS file can be edited and deployed independently. This is what makes theming a product feature rather than a development task.
The consistency problem. A design system that is not tokenized drifts. One component uses blue-600, another uses blue-500, a third uses a custom hex value that matches neither. Tokens enforce consistency because there is one place to define each value.
The scaling problem. Adding a new brand should not require touching every component. With tokens, adding a brand means adding a CSS file that defines the token values. The components remain unchanged.
a. Design tokens with CSS variables
A design token is a named value that a component references instead of a literal. The token layer is the set of CSS variables that define the values, and the Tailwind configuration maps the variables to utility classes.
The CSS variable layer
The variables are defined in :root for the default theme and in a scoped selector for each alternate theme.
:root {
--color-primary: 240 5.9% 10%;
--color-primary-foreground: 0 0% 98%;
--color-background: 0 0% 100%;
--color-foreground: 240 10% 3.9%;
--color-muted: 240 4.8% 95.9%;
--color-border: 240 5.9% 90%;
--radius: 0.5rem;
--font-sans: 'Inter', system-ui, sans-serif;
}
.dark {
--color-primary: 0 0% 98%;
--color-primary-foreground: 240 5.9% 10%;
--color-background: 240 10% 3.9%;
--color-foreground: 0 0% 98%;
--color-muted: 240 3.7% 15.9%;
--color-border: 240 3.7% 15.9%;
}
The values are stored as HSL channels without the hsl() wrapper. The hsl() function is applied in the Tailwind configuration. This allows Tailwind’s opacity modifiers, such as bg-primary/50, to work correctly.
The Tailwind configuration
The configuration maps each token to a utility namespace. The mapping is the bridge between the CSS variables and the utility classes.
// tailwind.config.js
export default {
darkMode: 'class',
theme: {
extend: {
colors: {
background: 'hsl(var(--color-background) / <alpha-value>)',
foreground: 'hsl(var(--color-foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--color-primary) / <alpha-value>)',
foreground: 'hsl(var(--color-primary-foreground) / <alpha-value>)',
},
muted: 'hsl(var(--color-muted) / <alpha-value>)',
border: 'hsl(var(--color-border) / <alpha-value>)',
},
borderRadius: {
DEFAULT: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
lg: 'var(--radius)',
xl: 'calc(var(--radius) + 4px)',
},
fontFamily: {
sans: 'var(--font-sans)',
},
},
},
};
The <alpha-value> placeholder is replaced by Tailwind with the opacity modifier. The bg-primary/50 utility produces hsl(var(--color-primary) / 0.5), which is a valid CSS color.
The component usage
A component uses the token-based utilities, never the literal colors.
function Button({ children }) {
return (
<button className="bg-primary text-primary-foreground rounded-lg px-4 py-2">
{children}
</button>
);
}
The button has no opinion about the primary color. The theme supplies it. Changing the theme changes the button’s appearance without editing the button.
The token naming convention
A token name describes the role, not the value. primary is a role; blue-600 is a value. The role-based name is what makes the component themeable, because the theme can map the role to any value.
| Role token | Bad alternative |
|---|---|
primary | blue |
destructive | red |
muted | gray |
accent | purple |
success | green |
The semantic layer
The token layer can have multiple levels: a primitive layer with raw values and a semantic layer that references them. The component uses the semantic layer, and the theme can redefine the semantic layer without touching the primitives.
:root {
/* Primitives */
--blue-600: 221 83% 53%;
--gray-900: 240 10% 3.9%;
--gray-50: 0 0% 98%;
/* Semantic */
--color-primary: var(--blue-600);
--color-foreground: var(--gray-900);
--color-background: var(--gray-50);
}
The semantic layer is what the components use, and the primitives are the raw values. Changing the brand means changing the semantic layer’s references.
b. Runtime theme switching
The theme can be switched at runtime by changing the class on the root element or by setting the CSS variables directly. The class-based approach is the most common and the simplest.
The class-based switch
The theme is selected by adding or removing a class on the <html> element. The dark class activates the dark theme.
function ThemeToggle() {
const [theme, setTheme] = useState('light');
function toggle() {
const next = theme === 'light' ? 'dark' : 'light';
setTheme(next);
document.documentElement.classList.toggle('dark', next === 'dark');
localStorage.setItem('theme', next);
}
return <button onClick={toggle}>Toggle theme</button>;
}
The class change is instant, because the CSS variables are re-evaluated by the browser. No reload is needed.
The initial theme
The theme must be applied before the first paint to avoid a flash of the wrong theme. The localStorage value is read in an inline script in the <head>.
<script>
(function () {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
})();
</script>
The script runs before the body renders, so the first paint uses the correct theme.
The system preference
The theme can follow the operating system’s preference, with an option for the user to override it. The prefers-color-scheme media query provides the system’s choice.
function useSystemTheme() {
const [system, setSystem] = useState('light');
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
setSystem(mq.matches ? 'dark' : 'light');
const handler = (e) => setSystem(e.matches ? 'dark' : 'light');
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return system;
}
The user’s stored preference takes priority over the system preference. The system preference is the fallback.
The three-state model
The theme has three states: light, dark, and system. The system state follows the operating system’s preference; the other two are explicit.
type Theme = 'light' | 'dark' | 'system';
The stored value is the user’s choice. The applied value is the resolved theme, which is system resolved to light or dark.
The flash prevention
The flash of the wrong theme happens when the HTML renders with the default theme before the script runs. The inline script in the <head> prevents it by applying the theme before the body renders. The server can also render the correct class if the theme is known on the server.
The transition
A theme switch can be animated with a CSS transition. The transition applies to the background-color and color properties.
:root {
transition: background-color 0.3s, color 0.3s;
}
The transition makes the switch feel smoother, but it should be disabled during the initial load to avoid animating from the default theme.
The meta tag
The browser’s address bar and theme color can be updated to match the theme.
useEffect(() => {
const meta = document.querySelector('meta[name="theme-color"]');
meta?.setAttribute('content', theme === 'dark' ? '#0a0a0a' : '#ffffff');
}, [theme]);
The meta tag updates the browser’s UI to match the page’s theme.
c. Multi-brand token architecture
A multi-brand application has several token sets, one per brand. The brand is selected at build time, at runtime, or per route.
The per-brand CSS files
Each brand is a CSS file that defines the same set of variables with different values.
/* brands/acme.css */
:root[data-brand='acme'] {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius: 0.5rem;
--font-sans: 'Inter', sans-serif;
}
/* brands/globex.css */
:root[data-brand='globex'] {
--color-primary: 142 71% 45%;
--color-primary-foreground: 0 0% 98%;
--radius: 0.25rem;
--font-sans: 'Roboto', sans-serif;
}
The brand is selected by setting the data-brand attribute on the <html> element. The variables are scoped to the attribute selector, so only the active brand’s values apply.
The brand selection
The brand is set on the root element, either at build time or at runtime.
function setBrand(brand) {
document.documentElement.setAttribute('data-brand', brand);
}
The attribute change triggers the CSS to re-evaluate, and every component that uses the tokens updates.
The per-route brand
A multi-tenant application can select the brand based on the route. The route’s parameter determines the brand.
function BrandLayout() {
const { brand } = useParams();
useEffect(() => {
document.documentElement.setAttribute('data-brand', brand);
}, [brand]);
return <Outlet />;
}
The brand changes when the route changes, and the components update instantly.
The brand-specific assets
Logos and images are brand-specific, and they are selected by the brand. The component reads the brand and chooses the asset.
function Logo() {
const brand = useBrand();
return <img src={`/logos/${brand}.svg`} alt={brand} />;
}
The asset path is derived from the brand, and the component does not hardcode any specific logo.
The token contract
Every brand must define the same set of tokens. A missing token produces an undefined value, which breaks the components that use it. The contract is enforced by a checklist, a test, or a TypeScript type that lists the required tokens.
const REQUIRED_TOKENS = [
'--color-primary',
'--color-primary-foreground',
'--color-background',
'--color-foreground',
'--radius',
'--font-sans',
] as const;
A build step or a test verifies that every brand defines every required token.
The multi-brand component
A component uses the tokens and is unaware of the brand. The same component renders in every brand.
function Button({ children }) {
return (
<button className="bg-primary text-primary-foreground rounded-lg px-4 py-2 font-sans">
{children}
</button>
);
}
The button’s appearance is determined entirely by the active brand’s tokens. The component source is identical for every brand.
The brand-specific overrides
A component may need a brand-specific override, such as a different border radius or a different shadow. The override is expressed as a token, not as a literal value in the component.
:root[data-brand='acme'] {
--button-radius: var(--radius);
}
:root[data-brand='globex'] {
--button-radius: 9999px;
}
The component uses rounded-[var(--button-radius)] or a Tailwind-mapped utility. The override lives in the brand’s CSS, not in the component.
The architecture comparison
| Approach | Build Time | Runtime | Component Changes |
|---|---|---|---|
| Separate CSS per brand | Yes | No | None |
| CSS variables per brand | No | Yes | None |
| Separate builds per brand | Yes | No | None |
| Forked components | Yes | No | Every brand |
The CSS variables approach is the most flexible, because the brand can be switched at runtime without a rebuild.
Complete Example Session
/* ============================================
PART 1: THE DEFAULT THEME
============================================ */
:root {
--color-background: 0 0% 100%;
--color-foreground: 240 10% 3.9%;
--color-primary: 240 5.9% 10%;
--color-primary-foreground: 0 0% 98%;
--color-muted: 240 4.8% 95.9%;
--color-border: 240 5.9% 90%;
--radius: 0.5rem;
--font-sans: 'Inter', system-ui, sans-serif;
}
/* ============================================
PART 2: THE DARK THEME
============================================ */
.dark {
--color-background: 240 10% 3.9%;
--color-foreground: 0 0% 98%;
--color-primary: 0 0% 98%;
--color-primary-foreground: 240 5.9% 10%;
--color-muted: 240 3.7% 15.9%;
--color-border: 240 3.7% 15.9%;
}
/* ============================================
PART 3: THE TAILWIND CONFIG
============================================ */
// tailwind.config.js
export default {
darkMode: 'class',
theme: {
extend: {
colors: {
background: 'hsl(var(--color-background) / <alpha-value>)',
foreground: 'hsl(var(--color-foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--color-primary) / <alpha-value>)',
foreground: 'hsl(var(--color-primary-foreground) / <alpha-value>)',
},
muted: 'hsl(var(--color-muted) / <alpha-value>)',
border: 'hsl(var(--color-border) / <alpha-value>)',
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
fontFamily: {
sans: 'var(--font-sans)',
},
},
},
};
/* ============================================
PART 4: THE COMPONENT
============================================ */
function Card({ children }) {
return (
<div className="bg-background text-foreground border border-border rounded-lg p-6 font-sans">
{children}
</div>
);
}
function Button({ children }) {
return (
<button className="bg-primary text-primary-foreground rounded-md px-4 py-2">
{children}
</button>
);
}
// No literal colors. Only tokens.
/* ============================================
PART 5: THE THEME TOGGLE
============================================ */
function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const stored = localStorage.getItem('theme');
const initial = stored || 'light';
setTheme(initial);
document.documentElement.classList.toggle('dark', initial === 'dark');
}, []);
function toggle() {
const next = theme === 'light' ? 'dark' : 'light';
setTheme(next);
document.documentElement.classList.toggle('dark', next === 'dark');
localStorage.setItem('theme', next);
}
return (
<button onClick={toggle} className="bg-muted text-foreground rounded-md px-3 py-1">
{theme === 'light' ? 'Dark' : 'Light'}
</button>
);
}
/* ============================================
PART 6: THE FLASH PREVENTION
============================================ */
// index.html
<script>
(function () {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
})();
</script>
/* ============================================
PART 7: THE SYSTEM PREFERENCE
============================================ */
function useSystemTheme() {
const [system, setSystem] = useState('light');
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
setSystem(mq.matches ? 'dark' : 'light');
const handler = (e) => setSystem(e.matches ? 'dark' : 'light');
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return system;
}
/* ============================================
PART 8: THE MULTI-BRAND TOKENS
============================================ */
:root[data-brand='acme'] {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius: 0.5rem;
--font-sans: 'Inter', sans-serif;
}
:root[data-brand='globex'] {
--color-primary: 142 71% 45%;
--color-primary-foreground: 0 0% 98%;
--radius: 0.25rem;
--font-sans: 'Roboto', sans-serif;
}
/* ============================================
PART 9: THE BRAND SWITCH
============================================ */
function BrandSwitcher() {
function setBrand(brand) {
document.documentElement.setAttribute('data-brand', brand);
localStorage.setItem('brand', brand);
}
return (
<div>
<button onClick={() => setBrand('acme')}>Acme</button>
<button onClick={() => setBrand('globex')}>Globex</button>
</div>
);
}
/* ============================================
PART 10: THE COMPLETE PICTURE
============================================ */
// 1. Define tokens in :root for the default theme.
// 2. Override tokens in .dark for the dark theme.
// 3. Map tokens to Tailwind namespaces in the config.
// 4. Use token utilities in components.
// 5. Switch themes by toggling the .dark class.
// 6. Define per-brand tokens in [data-brand="..."].
// 7. Switch brands by setting the data-brand attribute.
// 8. Validate the token contract with a build step.
// 9. Prevent the flash with an inline script.
// 10. The components never change.
The ten parts showed the default theme, the dark theme, the Tailwind config, the component, the theme toggle, the flash prevention, the system preference, the multi-brand tokens, the brand switch, and the complete picture.
Quick Reference
Token Layers
| Layer | Purpose | Example |
|---|---|---|
| Primitive | Raw values | --blue-600: 221 83% 53% |
| Semantic | Role mapping | --color-primary: var(--blue-600) |
| Component | Component-specific | --button-radius: var(--radius) |
Tailwind Mapping
| Namespace | CSS Variable | Utility |
|---|---|---|
| Colors | --color-primary | bg-primary |
| Radius | --radius | rounded-lg |
| Font | --font-sans | font-sans |
| Spacing | --spacing-* | p-4 |
| Shadow | --shadow-* | shadow-md |
Theme Switching
| Approach | Mechanism |
|---|---|
| Class | .dark on <html> |
| Attribute | [data-theme="dark"] |
| Media | prefers-color-scheme |
| Inline | style.setProperty |
Multi-Brand Approaches
| Approach | Runtime Switch | Component Changes |
|---|---|---|
| CSS variables | Yes | None |
| Separate CSS per brand | No | None |
| Separate builds | No | None |
| Forked components | No | Every brand |
Flash Prevention
| Mechanism | When |
|---|---|
Inline script in <head> | Before body renders |
| Server-rendered class | On initial HTML |
CSS @media | Before JavaScript loads |
Best Practices
✅ Do This:
/* Use semantic token names. */
--color-primary: 221 83% 53%; /* ✅ */
/* Scope themes to a class or attribute. */
.dark { --color-background: 240 10% 3.9%; } /* ✅ */
/* Map tokens to Tailwind namespaces. */
colors: { primary: 'hsl(var(--color-primary) / <alpha-value>)' } /* ✅ */
// Use token utilities in components.
<button className="bg-primary text-primary-foreground"> // ✅
// Toggle the theme with a class on the root.
document.documentElement.classList.toggle('dark', isDark); // ✅
// Prevent the flash with an inline script.
<script>/* apply theme */</script> // ✅
// Validate the token contract.
REQUIRED_TOKENS.forEach((t) => assert(t)); // ✅
❌ Don’t Do This:
// Don't use literal colors in components.
<button className="bg-blue-600"> // ❌
// Don't hardcode theme values in components.
style={{ backgroundColor: '#3b82f6' }} // ❌
// Don't forget the alpha-value placeholder.
'var(--color-primary)' // ❌ Breaks opacity modifiers
// Don't switch themes with a full reload.
window.location.reload() // ❌
// Don't omit tokens in a brand.
/* brand file with missing --color-primary */ // ❌
// Don't derive the dark theme from the light theme by a formula.
filter: invert(1) // ❌ Contrast suffers
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Flash of wrong theme | Script runs after render | Inline script in <head> |
| Opacity modifiers broken | Missing <alpha-value> | Add the placeholder |
| Brand token missing | No contract check | Add a validation step |
| Theme not applied | Class not set on root | Toggle on documentElement |
| Component shows wrong brand | Literal color used | Use a token utility |
| Transition animates on load | Transition always active | Disable during initial paint |
| System preference ignored | No media query | Use prefers-color-scheme |
Real-World Examples
1. Default Theme
:root { --color-primary: 240 5.9% 10%; }
2. Dark Theme
.dark { --color-primary: 0 0% 98%; }
3. Tailwind Mapping
colors: { primary: 'hsl(var(--color-primary) / <alpha-value>)' }
4. Token Component
<button className="bg-primary text-primary-foreground">
5. Theme Toggle
document.documentElement.classList.toggle('dark', next === 'dark');
6. Flash Prevention
<script>/* apply theme before render */</script>
7. System Preference
window.matchMedia('(prefers-color-scheme: dark)');
8. Brand Tokens
:root[data-brand='acme'] { --color-primary: 221 83% 53%; }
9. Brand Switch
document.documentElement.setAttribute('data-brand', brand);
10. Token Contract
REQUIRED_TOKENS.forEach((token) => assertDefined(token));
Visual
The Token Layer
┌────────────────────────────────────────────────────────────┐
│ TOKEN LAYERS │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PRIMITIVE │ │
│ │ --blue-600: 221 83% 53% │ │
│ │ --gray-900: 240 10% 3.9% │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SEMANTIC │ │
│ │ --color-primary: var(--blue-600) │ │
│ │ --color-foreground: var(--gray-900) │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ TAILWIND │ │
│ │ primary: hsl(var(--color-primary) / <alpha>) │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ COMPONENT │ │
│ │ <button className="bg-primary"> │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The component never sees a literal color. │
└────────────────────────────────────────────────────────────┘
Theme Switching
┌────────────────────────────────────────────────────────────┐
│ THEME SWITCHING │
│ │
│ <html class="dark"> │
│ │ │
│ │ The class is toggled at runtime. │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ .dark { │ │
│ │ --color-background: 240 10% 3.9%; │ │
│ │ --color-foreground: 0 0% 98%; │ │
│ │ } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ CSS variables re-evaluate │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Every component that uses bg-background │ │
│ │ re-renders with the new value. │ │
│ │ │ │
│ │ No reload. No component edit. Instant. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Multi-Brand Architecture
┌────────────────────────────────────────────────────────────┐
│ MULTI-BRAND TOKENS │
│ │
│ <html data-brand="acme"> │
│ ┌──────────────────────────────────────────────────┐ │
│ │ --color-primary: 221 83% 53% (blue) │ │
│ │ --radius: 0.5rem │ │
│ │ --font-sans: 'Inter' │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ <html data-brand="globex"> │
│ ┌──────────────────────────────────────────────────┐ │
│ │ --color-primary: 142 71% 45% (green) │ │
│ │ --radius: 0.25rem │ │
│ │ --font-sans: 'Roboto' │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <Button className="bg-primary rounded-lg"> │ │
│ │ │ │
│ │ The same component renders in both brands. │ │
│ │ The tokens supply the difference. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Flash Prevention
┌────────────────────────────────────────────────────────────┐
│ FLASH PREVENTION │
│ │
│ WITHOUT INLINE SCRIPT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Browser renders default (light) theme. │ │
│ │ 2. JavaScript loads. │ │
│ │ 3. Theme is read from localStorage. │ │
│ │ 4. Class is applied. │ │
│ │ 5. Dark theme appears. │ │
│ │ │ │
│ │ The user sees a white flash. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH INLINE SCRIPT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <head> │ │
│ │ <script> │ │
│ │ // Read theme, apply class. │ │
│ │ </script> │ │
│ │ </head> │ │
│ │ │ │
│ │ 1. Script runs before body renders. │ │
│ │ 2. Class is applied. │ │
│ │ 3. Body renders with the correct theme. │ │
│ │ │ │
│ │ No flash. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Design token | A named value that components reference |
| CSS variable | The mechanism that stores the token |
| Semantic token | A role-based name like primary |
| Primitive token | A raw value like blue-600 |
| Tailwind mapping | The bridge from variables to utilities |
<alpha-value> | The placeholder for opacity modifiers |
| Theme switching | Toggling a class on the root element |
| Flash prevention | An inline script in the <head> |
| System preference | prefers-color-scheme media query |
| Multi-brand | Per-brand token sets scoped by attribute |
| Token contract | The set of tokens every brand must define |
Key takeaways:
- Components use tokens, never literal values. A component that writes
bg-primaryis a component that can render in any theme. A component that writesbg-blue-600is tied to one brand. The token layer is what makes the difference. - CSS variables are the runtime mechanism. The variables are defined in
:rootfor the default theme and overridden in a scoped selector for each alternate theme. The browser re-evaluates them when the selector’s condition changes, and every component that uses them updates instantly. - The Tailwind configuration maps the variables to utilities. The
<alpha-value>placeholder is what makes opacity modifiers likebg-primary/50work. Without it, the opacity modifier is ignored. - The semantic layer is what the components use. A token named
primaryis a role, not a value. The theme maps the role to a value. Changing the theme changes the mapping, and the components update without edits. - The theme is toggled by a class on the root. The
darkclass activates the dark theme’s variables. The toggle is instant, and no reload is needed. - The flash of the wrong theme is prevented by an inline script. The script reads the stored theme and applies the class before the body renders. Without it, the page renders with the default theme and then switches, producing a visible flash.
- The system preference is the fallback. The
prefers-color-schememedia query detects the operating system’s choice. The user’s explicit choice overrides it, and the system preference is used when no choice is stored. - Multi-brand is the same mechanism with more token sets. Each brand is a CSS file that defines the same token names with different values. The brand is selected by an attribute on the root element. The components are identical for every brand.
- The token contract must be enforced. Every brand must define every required token. A missing token produces an undefined value and a broken component. A build step or a test verifies the contract.
Remember: Design tokens and dynamic theming turn colors, fonts, and radii from hardcoded values into named roles that the theme supplies. The components reference the roles, the theme defines the values, and the Tailwind configuration bridges the two. Theme switching changes the class on the root element, and the CSS variables re-evaluate instantly. Multi-brand replaces the class with an attribute and the token set with a per-brand file. The flash of the wrong theme is prevented by an inline script that runs before the body renders. The result is a component tree that renders in any theme or brand without a single edit, and a theme layer that can be changed at runtime, at build time, or per route.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!