CSS Theming with Custom Properties
Once you're comfortable with CSS Custom Properties, theming is the natural next step: define every design token — color, spacing, radius — once, and let swapping a single class or data attribute on a parent element change the entire page. No separate stylesheet per theme, no duplicated rule sets, just one set of components reading from variables that mean different things depending on context.
Defining tokens at the root
CSS
:root {
--color-bg: #ffffff;
--color-surface: #f5f5f5;
--color-text: #1a1a1a;
--color-primary: #2a6df4;
--color-border: #dddddd;
--radius-md: 8px;
--space-md: 16px;
}Swapping a theme with a class or data attribute
Rather than gating theme variables behind a media query alone, define a selector for each named theme and let something in your application (a toggle button, a saved user preference) apply it by setting a class or attribute on a high-level element like
<html> or <body>.CSS
/* Light theme (also the default, at :root) */
:root,
[data-theme="light"] {
--color-bg: #ffffff;
--color-surface: #f5f5f5;
--color-text: #1a1a1a;
--color-primary: #2a6df4;
--color-border: #dddddd;
}
/* Dark theme */
[data-theme="dark"] {
--color-bg: #121212;
--color-surface: #1e1e1e;
--color-text: #f0f0f0;
--color-primary: #6fa1ff;
--color-border: #3a3a3a;
}
/* A second brand — same variable names, entirely different palette */
[data-theme="brand-orange"] {
--color-bg: #fff8f0;
--color-surface: #ffeedd;
--color-text: #2a1a0a;
--color-primary: #e07a1f;
--color-border: #f0c9a0;
}
body {
background: var(--color-bg);
color: var(--color-text);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.btn--primary {
background: var(--color-primary);
color: white;
}JS
// Switching themes at runtime is just changing an attribute
document.documentElement.setAttribute('data-theme', 'dark');
// Persist the choice
localStorage.setItem('theme', 'dark');Organizing tokens: semantic names, not literal ones
The single most important habit in a scalable theming system is naming variables for what they mean, not for what color they currently happen to be. A variable named
--blue becomes actively misleading the moment a dark theme needs that same role filled by a lighter blue — but a variable named --color-primary can hold whatever value each theme needs without ever renaming the variable itself.CSS
/* Avoid: literal names tie the variable to one specific value */
:root {
--blue: #2a6df4;
--gray-900: #1a1a1a;
}
/* Prefer: semantic names describe the role the color plays */
:root {
--color-primary: #2a6df4;
--color-text: #1a1a1a;
}
/* Now a theme can safely redefine the role without renaming anything
that consumes it */
[data-theme="dark"] {
--color-primary: #6fa1ff; /* still "primary", just a different blue */
--color-text: #f0f0f0;
}Note
A useful two-layer pattern for larger design systems: keep a set of literal, low-level tokens (
--blue-500, --gray-900) that never change, then map semantic tokens (--color-primary) onto them per theme. Components only ever reference the semantic layer.Scalability
This pattern scales in a way that maintaining separate stylesheets per theme never does: adding a third or fourth theme is just adding another selector block of variable overrides, not duplicating and re-editing every component's CSS. The component rules themselves never need to know how many themes exist.