Skip to content
CSS

Theming with CSS Variables

Build flexible themes with CSS custom properties — dark mode, brand themes, and dynamic switching.

stellae.design

9 min read

Introduction

CSS custom properties are live in the browser — you can change them at runtime with JavaScript, scope them to components, and cascade them through the DOM. This makes them perfect for theming.

Whether you're implementing dark mode, brand themes for white-label products, or user-customizable interfaces, CSS variables provide the mechanism.

This guide covers theme variable architecture, switching themes, scoping variables, and integrating with Tailwind and React.


Key Concepts

Defining Theme Variables

css
:root {
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f9fafb;
  --color-text-primary: #111827;
  --color-text-secondary: #6b7280;
  --color-accent: #3b82f6;
  --color-border: #e5e7eb;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --radius-md: 0.5rem;
}

[data-theme="dark"] {
  --color-bg-primary: #111827;
  --color-bg-secondary: #1f2937;
  --color-text-primary: #f9fafb;
  --color-text-secondary: #9ca3af;
  --color-accent: #60a5fa;
  --color-border: #374151;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
}

Using Variables

css
.card {
  background: var(--color-bg-primary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text-primary);
  box-shadow: var(--shadow-sm);
}

Practical Examples

1. Theme Switcher Hook

typescript
'use client';
import { useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

export function useTheme() {
  const [theme, setTheme] = useState<Theme>('system');
  useEffect(() => {
    const stored = localStorage.getItem('theme') as Theme;
    if (stored) setTheme(stored);
  }, []);
  useEffect(() => {
    const root = document.documentElement;
    if (theme === 'system') root.removeAttribute('data-theme');
    else root.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);
  }, [theme]);
  return { theme, setTheme };
}

2. Brand Themes for White-Label

css
[data-brand="brand-a"] {
  --color-accent: #e11d48;
  --font-sans: 'Poppins', sans-serif;
  --radius-md: 1rem;
}

[data-brand="brand-b"] {
  --color-accent: #059669;
  --font-sans: 'DM Sans', sans-serif;
  --radius-md: 0.25rem;
}

3. Tailwind Integration

typescript
// tailwind.config.ts
const config = {
  theme: {
    extend: {
      colors: {
        bg: { primary: 'var(--color-bg-primary)', secondary: 'var(--color-bg-secondary)' },
        text: { primary: 'var(--color-text-primary)' },
        accent: { DEFAULT: 'var(--color-accent)' },
      },
    },
  },
};

// Usage
<div className="bg-bg-primary text-text-primary">
  <button className="bg-accent text-white rounded px-4 py-2">Themed</button>
</div>

4. Preventing Flash of Wrong Theme

html
// Inline script in <head> to apply theme before paint
<script dangerouslySetInnerHTML={{ __html: `
  (function() {
    var t = localStorage.getItem('theme');
    if (t && t !== 'system') document.documentElement.setAttribute('data-theme', t);
    else if (window.matchMedia('(prefers-color-scheme: dark)').matches)
      document.documentElement.setAttribute('data-theme', 'dark');
  })()
` }} />

Best Practices

  • ✅ Define variables on :root, override on [data-theme] selectors
  • ✅ Prevent flash of wrong theme with inline script in <head>
  • ✅ Support system preference with prefers-color-scheme
  • ✅ Use semantic names (bg-primary not white) for theme independence
  • ✅ Scope component-specific variables with BEM-like naming
  • ❌ Don't mix hardcoded colors with variables — all themed values should be variables
  • ❌ Don't nest custom properties too deeply — keep the cascade simple

Common Pitfalls

  • Flash of unstyled content — inline script must run before CSS loads
  • Forgetting to theme shadows and borders — they look wrong in dark mode
  • Using rgba() with variables — CSS variables can't be used inside rgba(), use separate channels
  • Not testing all themes — easy to forget hover states and focus rings

CSScss

Was this article helpful?