CSS Custom Properties: Theming Systems
Building a Theming System
CSS Custom Properties are ideal for creating flexible theming systems that allow users to customize the appearance of your application. In this guide, we'll explore different approaches to implementing themes.
Theme Architecture
A well-structured theming system typically includes:
- Base Variables: Foundational values that don't change between themes
- Theme Variables: Values that change based on the active theme
- Semantic Variables: Purpose-based variables that reference theme variables
- Component Variables: Component-specific variables that reference semantic variables
/* Base Variables */
:root {
/* Spacing */
--spacing-unit: 8px;
--spacing-xs: calc(var(--spacing-unit) * 0.5);
--spacing-sm: var(--spacing-unit);
--spacing-md: calc(var(--spacing-unit) * 2);
--spacing-lg: calc(var(--spacing-unit) * 3);
--spacing-xl: calc(var(--spacing-unit) * 4);
/* Typography */
--font-family-base: 'Inter', sans-serif;
--font-family-heading: 'Inter', sans-serif;
--font-size-base: 16px;
--line-height-base: 1.5;
/* Borders */
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-width: 1px;
}
/* Theme Variables (Light Theme) */
:root {
/* Colors */
--color-primary-h: 210;
--color-primary-s: 90%;
--color-primary-l: 50%;
--color-primary: hsl(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l));
--color-secondary-h: 150;
--color-secondary-s: 60%;
--color-secondary-l: 45%;
--color-secondary: hsl(var(--color-secondary-h), var(--color-secondary-s), var(--color-secondary-l));
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #212529;
--color-text-muted: #6c757d;
--color-border: #dee2e6;
/* Semantic Variables */
--color-success: #28a745;
--color-warning: #ffc107;
--color-danger: #dc3545;
--color-info: #17a2b8;
/* Effects */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
/* Component Variables */
.button {
--button-padding: var(--spacing-sm) var(--spacing-md);
--button-border-radius: var(--border-radius-md);
--button-font-weight: 500;
padding: var(--button-padding);
border-radius: var(--button-border-radius);
font-weight: var(--button-font-weight);
}
.button-primary {
--button-bg: var(--color-primary);
--button-color: white;
--button-border: none;
background-color: var(--button-bg);
color: var(--button-color);
border: var(--button-border);
}
Light and Dark Themes
Theme Switching with CSS Classes
One common approach is to use a class on the <html>
or <body>
element to switch themes:
/* Light theme (default) */
:root {
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #212529;
--color-text-muted: #6c757d;
--color-border: #dee2e6;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* Dark theme */
.dark-theme {
--color-background: #121212;
--color-surface: #1e1e1e;
--color-text: #e0e0e0;
--color-text-muted: #a0a0a0;
--color-border: #333333;
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Using the theme variables */
body {
background-color: var(--color-background);
color: var(--color-text);
}
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
box-shadow: 0 4px 6px var(--shadow-color);
}
/* JavaScript for theme switching */
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark-theme');
// Save preference
const isDarkTheme = document.documentElement.classList.contains('dark-theme');
localStorage.setItem('darkTheme', isDarkTheme);
});
// Check for saved theme preference
const savedTheme = localStorage.getItem('darkTheme');
if (savedTheme === 'true') {
document.documentElement.classList.add('dark-theme');
}
Using prefers-color-scheme
Respect user's system preference for light or dark themes:
/* Light theme (default) */
:root {
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #212529;
/* other light theme variables */
}
/* Dark theme based on system preference */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #121212;
--color-surface: #1e1e1e;
--color-text: #e0e0e0;
/* other dark theme variables */
}
}
/* Allow user to override system preference */
.light-theme {
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #212529;
/* other light theme variables */
}
.dark-theme {
--color-background: #121212;
--color-surface: #1e1e1e;
--color-text: #e0e0e0;
/* other dark theme variables */
}
// JavaScript for theme control
const themeSelect = document.getElementById('theme-select');
themeSelect.addEventListener('change', (e) => {
// Remove any existing theme classes
document.documentElement.classList.remove('light-theme', 'dark-theme');
const selectedTheme = e.target.value;
if (selectedTheme !== 'system') {
document.documentElement.classList.add(`${selectedTheme}-theme`);
}
// Save preference
localStorage.setItem('theme', selectedTheme);
});
// Check for saved theme preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme && savedTheme !== 'system') {
document.documentElement.classList.add(`${savedTheme}-theme`);
}
Color Scheme Generation
HSL-Based Color Schemes
Using HSL colors makes it easy to create harmonious color schemes by manipulating hue, saturation, and lightness:
:root {
/* Base hue for the theme */
--hue-primary: 210; /* Blue */
/* Primary color and variations */
--color-primary: hsl(var(--hue-primary), 90%, 50%);
--color-primary-light: hsl(var(--hue-primary), 90%, 65%);
--color-primary-dark: hsl(var(--hue-primary), 90%, 35%);
/* Complementary color (opposite on the color wheel) */
--hue-complementary: calc(var(--hue-primary) + 180);
--color-complementary: hsl(var(--hue-complementary), 90%, 50%);
/* Analogous colors (adjacent on the color wheel) */
--hue-analogous-1: calc(var(--hue-primary) - 30);
--hue-analogous-2: calc(var(--hue-primary) + 30);
--color-analogous-1: hsl(var(--hue-analogous-1), 90%, 50%);
--color-analogous-2: hsl(var(--hue-analogous-2), 90%, 50%);
/* Triadic colors (evenly spaced on the color wheel) */
--hue-triadic-1: calc(var(--hue-primary) + 120);
--hue-triadic-2: calc(var(--hue-primary) + 240);
--color-triadic-1: hsl(var(--hue-triadic-1), 90%, 50%);
--color-triadic-2: hsl(var(--hue-triadic-2), 90%, 50%);
}
/* JavaScript to change the entire color scheme by adjusting one value */
function updateColorScheme(hue) {
document.documentElement.style.setProperty('--hue-primary', hue);
}
// Color picker for hue selection
const huePicker = document.getElementById('hue-picker');
huePicker.addEventListener('input', (e) => {
updateColorScheme(e.target.value);
});
Generating Shades and Tints
Create a range of shades and tints from a base color:
:root {
/* Base color values */
--color-primary-h: 210;
--color-primary-s: 90%;
--color-primary-l: 50%;
/* Generate shades (darker) and tints (lighter) */
--color-primary-100: hsl(var(--color-primary-h), var(--color-primary-s), 90%);
--color-primary-200: hsl(var(--color-primary-h), var(--color-primary-s), 80%);
--color-primary-300: hsl(var(--color-primary-h), var(--color-primary-s), 70%);
--color-primary-400: hsl(var(--color-primary-h), var(--color-primary-s), 60%);
--color-primary-500: hsl(var(--color-primary-h), var(--color-primary-s), 50%);
--color-primary-600: hsl(var(--color-primary-h), var(--color-primary-s), 40%);
--color-primary-700: hsl(var(--color-primary-h), var(--color-primary-s), 30%);
--color-primary-800: hsl(var(--color-primary-h), var(--color-primary-s), 20%);
--color-primary-900: hsl(var(--color-primary-h), var(--color-primary-s), 10%);
/* Base color shorthand */
--color-primary: var(--color-primary-500);
}
/* JavaScript to generate a full color palette from a single color */
function updatePalette(hue, saturation) {
const root = document.documentElement.style;
root.setProperty('--color-primary-h', hue);
root.setProperty('--color-primary-s', saturation + '%');
// You could also generate secondary, tertiary colors, etc.
const secondaryHue = (parseInt(hue) + 120) % 360;
root.setProperty('--color-secondary-h', secondaryHue);
root.setProperty('--color-secondary-s', saturation + '%');
}
Multiple Theme Support
Theme Definitions
Create multiple complete themes that users can switch between:
/* Default theme */
:root {
/* Common variables that don't change between themes */
--spacing-unit: 8px;
--border-radius: 4px;
/* etc. */
}
/* Light theme (default) */
:root {
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #212529;
--color-primary: #0d6efd;
--color-secondary: #6c757d;
}
/* Dark theme */
.theme-dark {
--color-background: #121212;
--color-surface: #1e1e1e;
--color-text: #e0e0e0;
--color-primary: #90caf9;
--color-secondary: #b0bec5;
}
/* Nature theme */
.theme-nature {
--color-background: #f1f8e9;
--color-surface: #ffffff;
--color-text: #33691e;
--color-primary: #689f38;
--color-secondary: #8bc34a;
}
/* Ocean theme */
.theme-ocean {
--color-background: #e1f5fe;
--color-surface: #ffffff;
--color-text: #01579b;
--color-primary: #0288d1;
--color-secondary: #29b6f6;
}
/* High contrast theme for accessibility */
.theme-high-contrast {
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-text: #000000;
--color-primary: #0000ee;
--color-secondary: #551a8b;
--border-width: 2px;
--focus-outline-width: 3px;
}
Theme Switcher Implementation
<!-- HTML for theme switcher -->
<div class="theme-switcher">
<label for="theme-select">Theme:</label>
<select id="theme-select">
<option value="default">Light (Default)</option>
<option value="dark">Dark</option>
<option value="nature">Nature</option>
<option value="ocean">Ocean</option>
<option value="high-contrast">High Contrast</option>
</select>
</div>
// JavaScript for theme switching
const themeSelect = document.getElementById('theme-select');
const html = document.documentElement;
// Function to set theme
function setTheme(themeName) {
// Remove all theme classes
html.classList.remove('theme-dark', 'theme-nature', 'theme-ocean', 'theme-high-contrast');
// Add selected theme class if not default
if (themeName !== 'default') {
html.classList.add(`theme-${themeName}`);
}
// Save preference
localStorage.setItem('theme', themeName);
}
// Event listener for theme selection
themeSelect.addEventListener('change', (e) => {
setTheme(e.target.value);
});
// Load saved theme on page load
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
themeSelect.value = savedTheme;
setTheme(savedTheme);
}
});
User Customizable Themes
Theme Editor Interface
Allow users to create their own custom themes:
<!-- HTML for theme editor -->
<div class="theme-editor">
<h3>Customize Theme</h3>
<div class="color-picker">
<label for="primary-color">Primary Color:</label>
<input type="color" id="primary-color" value="#0d6efd">
</div>
<div class="color-picker">
<label for="secondary-color">Secondary Color:</label>
<input type="color" id="secondary-color" value="#6c757d">
</div>
<div class="color-picker">
<label for="background-color">Background Color:</label>
<input type="color" id="background-color" value="#ffffff">
</div>
<div class="color-picker">
<label for="text-color">Text Color:</label>
<input type="color" id="text-color" value="#212529">
</div>
<div class="range-picker">
<label for="border-radius">Border Radius: <span id="radius-value">4px</span></label>
<input type="range" id="border-radius" min="0" max="20" value="4">
</div>
<div class="range-picker">
<label for="spacing-unit">Spacing Unit: <span id="spacing-value">8px</span></label>
<input type="range" id="spacing-unit" min="4" max="16" value="8">
</div>
<button id="save-theme">Save Theme</button>
<button id="reset-theme">Reset to Default</button>
</div>
// JavaScript for theme editor
document.addEventListener('DOMContentLoaded', () => {
// Color pickers
const primaryColorPicker = document.getElementById('primary-color');
const secondaryColorPicker = document.getElementById('secondary-color');
const backgroundColorPicker = document.getElementById('background-color');
const textColorPicker = document.getElementById('text-color');
// Range inputs
const borderRadiusInput = document.getElementById('border-radius');
const spacingUnitInput = document.getElementById('spacing-unit');
// Value displays
const radiusValue = document.getElementById('radius-value');
const spacingValue = document.getElementById('spacing-value');
// Buttons
const saveButton = document.getElementById('save-theme');
const resetButton = document.getElementById('reset-theme');
// Default values
const defaultTheme = {
primaryColor: '#0d6efd',
secondaryColor: '#6c757d',
backgroundColor: '#ffffff',
textColor: '#212529',
borderRadius: '4',
spacingUnit: '8'
};
// Load saved theme or defaults
function loadTheme() {
const savedTheme = JSON.parse(localStorage.getItem('customTheme')) || defaultTheme;
// Update inputs to match saved values
primaryColorPicker.value = savedTheme.primaryColor;
secondaryColorPicker.value = savedTheme.secondaryColor;
backgroundColorPicker.value = savedTheme.backgroundColor;
textColorPicker.value = savedTheme.textColor;
borderRadiusInput.value = savedTheme.borderRadius;
spacingUnitInput.value = savedTheme.spacingUnit;
// Update displays
radiusValue.textContent = savedTheme.borderRadius + 'px';
spacingValue.textContent = savedTheme.spacingUnit + 'px';
// Apply theme
applyTheme(savedTheme);
}
// Apply theme to CSS variables
function applyTheme(theme) {
const root = document.documentElement.style;
root.setProperty('--color-primary', theme.primaryColor);
root.setProperty('--color-secondary', theme.secondaryColor);
root.setProperty('--color-background', theme.backgroundColor);
root.setProperty('--color-text', theme.textColor);
root.setProperty('--border-radius', theme.borderRadius + 'px');
root.setProperty('--spacing-unit', theme.spacingUnit + 'px');
}
// Save current theme
function saveTheme() {
const customTheme = {
primaryColor: primaryColorPicker.value,
secondaryColor: secondaryColorPicker.value,
backgroundColor: backgroundColorPicker.value,
textColor: textColorPicker.value,
borderRadius: borderRadiusInput.value,
spacingUnit: spacingUnitInput.value
};
localStorage.setItem('customTheme', JSON.stringify(customTheme));
alert('Theme saved!');
}
// Reset to defaults
function resetTheme() {
localStorage.removeItem('customTheme');
loadTheme();
alert('Theme reset to defaults');
}
// Event listeners for inputs
primaryColorPicker.addEventListener('input', () => {
document.documentElement.style.setProperty('--color-primary', primaryColorPicker.value);
});
secondaryColorPicker.addEventListener('input', () => {
document.documentElement.style.setProperty('--color-secondary', secondaryColorPicker.value);
});
backgroundColorPicker.addEventListener('input', () => {
document.documentElement.style.setProperty('--color-background', backgroundColorPicker.value);
});
textColorPicker.addEventListener('input', () => {
document.documentElement.style.setProperty('--color-text', textColorPicker.value);
});
borderRadiusInput.addEventListener('input', () => {
const value = borderRadiusInput.value + 'px';
radiusValue.textContent = value;
document.documentElement.style.setProperty('--border-radius', value);
});
spacingUnitInput.addEventListener('input', () => {
const value = spacingUnitInput.value + 'px';
spacingValue.textContent = value;
document.documentElement.style.setProperty('--spacing-unit', value);
});
// Button event listeners
saveButton.addEventListener('click', saveTheme);
resetButton.addEventListener('click', resetTheme);
// Initialize
loadTheme();
});
Best Practices for Theming
Naming Conventions
- Use Semantic Names: Name variables by their purpose, not their value (e.g.,
--color-primary
not--color-blue
) - Create a Hierarchy: Organize variables from generic to specific
- Be Consistent: Follow the same naming pattern throughout your codebase
- Use Prefixes: Group related variables with prefixes (e.g.,
--color-
,--spacing-
)
Performance Considerations
- Minimize Recalculations: Change theme variables in batches to reduce layout recalculations
- Use Classes When Possible: For major theme changes, toggling classes can be more efficient than changing many individual properties
- Consider Scoping: Apply theme changes to specific components rather than the entire document when appropriate
- Test Performance: Measure the impact of theme changes, especially on mobile devices
Accessibility
- Maintain Contrast: Ensure text remains readable when theme colors change
- Respect User Preferences: Honor
prefers-color-scheme
and other user settings - Provide High Contrast Option: Include a high contrast theme for users with visual impairments
- Test with Assistive Technology: Verify that theme changes don't impact screen readers or other assistive tech
Next Steps
Now that you understand how to build theming systems with CSS Custom Properties, you can explore more advanced topics in our other guides:
- CSS Custom Properties: JavaScript Integration
- CSS Custom Properties: Advanced Responsive Techniques
- CSS Custom Properties: Integration with Preprocessors