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
Pro Tip: Consider using CSS custom properties not just for colors, but for all aspects of your design system: spacing, typography, animations, etc. This creates a comprehensive theming system that can be customized as a whole.

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:

Remember: A well-designed theming system improves both user experience and developer experience. Users get customization options, while developers get a more maintainable codebase.