DEV Community

Cover image for Beyond Light/Dark Mode: Implementing Dynamic Themes with CSS Custom Properties
CodeWithDhanian
CodeWithDhanian

Posted on

Beyond Light/Dark Mode: Implementing Dynamic Themes with CSS Custom Properties

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)