For years, the pinnacle of user-centric theming was a simple light/dark mode toggle. While this is a great start, modern web design demands more flexibility. Users expect interfaces that adapt not just to their system preferences but to their personal tastes—whether that's a calming blue hue, a high-contrast theme for accessibility, or even their favorite brand colors.
The key to building these dynamic, user-configurable themes lies in a powerful modern CSS feature: CSS Custom Properties (also known as CSS Variables). This article will guide you through moving beyond a simple binary switch to implementing a robust, multi-theme system.
Why CSS Custom Properties are Perfect for Theming
Unlike preprocessor variables (like those in SASS or LESS) which are compiled away and static, CSS Custom Properties are live variables in the browser. This means their values can be updated dynamically at runtime using JavaScript, making them the ideal foundation for any theme-switching functionality.
Their core benefits include:
- Dynamic Updates: Change a variable's value, and the change propagates instantly across all elements that use it.
- Scoping: Variables can be defined globally or scoped to specific components, allowing for nuanced design systems.
- Cascade and Inheritance: They follow the same rules as other CSS properties, providing a predictable and powerful way to manage styles.
Step 1: Defining Your Theme Palette with Custom Properties
The first step is to move away from hard-coded color values. Instead of writing color: #333;
everywhere, we define a set of semantic variables that represent the purpose of a color.
We typically define these variables on the :root
element to make them globally available.
:root {
/* Primary Colors */
--color-primary: 56, 73, 214; /* RGB format for alpha channel flexibility */
--color-secondary: 255, 196, 0;
/* Neutral Colors */
--color-background: 253, 253, 253;
--color-surface: 255, 255, 255;
--color-text: 10, 10, 10;
--color-text-muted: 115, 115, 115;
/* Spacing and other tokens */
--border-radius: 0.75rem;
--spacing-unit: 1rem;
}
To use these variables, we employ the var()
function. Notice how we use the RGB values; this allows us to control opacity easily with rgba()
.
body {
background-color: rgb(var(--color-background));
color: rgb(var(--color-text));
font-family: sans-serif;
padding: var(--spacing-unit);
}
.card {
background-color: rgb(var(--color-surface));
padding: calc(var(--spacing-unit) * 2);
border-radius: var(--border-radius);
border: 1px solid rgba(var(--color-text), 0.1);
}
.button {
background-color: rgba(var(--color-primary), 1);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
}
.button:hover {
background-color: rgba(var(--color-primary), 0.8);
}
Step 2: Creating Multiple Theme Definitions
A simple light/dark mode switch is just a special case of this system. We can define multiple themes by overriding the variable values within a class or attribute selector.
Method 1: Using a Class (e.g., .theme-dark
)
/* Default is light theme (defined in :root) */
:root {
--color-primary: 56, 73, 214;
--color-background: 253, 253, 253;
--color-surface: 255, 255, 255;
--color-text: 10, 10, 10;
}
/* Dark theme */
.theme-dark {
--color-background: 10, 10, 20;
--color-surface: 30, 30, 45;
--color-text: 245, 245, 245;
}
/* High Contrast Theme */
.theme-high-contrast {
--color-background: 255, 255, 255;
--color-surface: 255, 255, 255;
--color-text: 0, 0, 0;
--color-primary: 0, 0, 0;
}
Method 2: Using a Data Attribute (e.g., [data-theme="dark"]
)
This method is often preferred for its clarity and specificity.
[data-theme="dark"] {
--color-background: 10, 10, 20;
--color-surface: 30, 30, 45;
--color-text: 245, 245, 245;
}
[data-theme="high-contrast"] {
--color-background: 255, 255, 255;
--color-surface: 255, 255, 255;
--color-text: 0, 0, 0;
}
The beauty of this approach is that our component CSS never changes. The .card
and .button
styles remain the same; they automatically adapt because they reference the variables.
Step 3: The Magic: Switching Themes with JavaScript
Applying the theme is a matter of toggling the class or data attribute on the HTML element. This is a simple, clean JavaScript operation.
// Get the theme selector element (e.g., a dropdown)
const themeSelector = document.getElementById('theme-selector');
// Function to switch the theme
function setTheme(themeName) {
// Method 1: Using a class
// document.documentElement.className = themeName;
// Method 2: Using a data attribute (Recommended)
document.documentElement.setAttribute('data-theme', themeName);
// Optional: Save the user's preference to localStorage
localStorage.setItem('selectedTheme', themeName);
}
// Event listener for the theme selector
themeSelector.addEventListener('change', (e) => {
setTheme(e.target.value);
});
// On page load, check for a saved theme preference
window.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('selectedTheme') || 'light'; // default to 'light'
setTheme(savedTheme);
// Set the dropdown to the correct value
themeSelector.value = savedTheme;
});
Taking it Further: A User-Driven Color Picker
The real power is allowing users to define part of the theme themselves. Let's add a color picker to let users choose their primary color.
HTML:
<label for="primary-color-picker">Choose your primary color:</label>
<input type="color" id="primary-color-picker" value="#3849d6">
JavaScript:
const colorPicker = document.getElementById('primary-color-picker');
colorPicker.addEventListener('input', (e) => {
// Convert hex color to RGB string
const hexToRgb = (hex) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
};
const newRgbValue = hexToRgb(e.target.value);
// Update the CSS Custom Property directly on the root element
document.documentElement.style.setProperty('--color-primary', newRgbValue);
// Save the custom color
localStorage.setItem('customPrimaryColor', newRgbValue);
});
// Load custom color on page load
const savedColor = localStorage.getItem('customPrimaryColor');
if (savedColor) {
document.documentElement.style.setProperty('--color-primary', savedColor);
// Update the color picker's value (requires RGB to Hex conversion, omitted for brevity)
}
Conclusion: A Foundation for Expressive UIs
By leveraging CSS Custom Properties, we can move far beyond a simple light/dark binary. We can create a theming system that is:
- Maintainable: All design tokens are defined in one logical place.
- Extensible: Adding a new theme is as simple as adding a new block of CSS.
- User-empowering: Users can personalize their experience, leading to higher engagement and accessibility.
This approach forms the backbone of modern design systems used by companies like Google, Apple, and GitHub. Start implementing it in your projects to build more dynamic, flexible, and user-friendly web interfaces.
Master CSS from Foundations to Advanced Concepts
If you're excited about harnessing the full power of modern CSS like Custom Properties, Grid, Flexbox, and Animations, I highly recommend diving deeper with a structured resource.
To help you on your journey, check out my eBook, "CSS Unleashed: A Modern Guide to Responsive and Elegant Styling." It's designed to take you from core fundamentals to advanced techniques, with practical examples and best practices that you can immediately apply to your projects.
👉 Get your copy of "CSS Unleashed" here!
Happy coding
Top comments (0)