--- name: dark-mode description: Implements theme switching with semantic tokens and prefers-color-scheme detection. Use when adding dark mode, light/dark themes, color scheme toggles, or converting hardcoded colors to theme-aware tokens. --- # Dark Mode Implementation ## Overview Implement robust dark mode using semantic color tokens, CSS custom properties, and system preference detection. Creates a theme system that's maintainable, accessible, and works across frameworks. ## When to Use - Adding dark mode to an existing project - Setting up a theme system from scratch - Converting hardcoded colors to semantic tokens - Implementing system preference detection - Building a theme toggle component ## Quick Reference: Theme Architecture | Layer | Purpose | Example | |-------|---------|---------| | Primitive tokens | Raw color values | `--color-blue-500: #3b82f6` | | Semantic tokens | Purpose-based aliases | `--color-primary: var(--color-blue-500)` | | Theme tokens | Mode-specific values | `--color-bg: var(--color-gray-50)` in light | | Component tokens | Component-specific | `--button-bg: var(--color-primary)` | ## The Process 1. **Audit existing colors**: Find all color usage in codebase 2. **Define primitive palette**: Use color-scale skill if needed 3. **Create semantic tokens**: Map primitives to purposes 4. **Define theme tokens**: Light and dark values 5. **Choose switching mechanism**: CSS classes, data attributes, or media query 6. **Add system detection**: `prefers-color-scheme` 7. **Implement persistence**: localStorage or cookies 8. **Handle edge cases**: Images, shadows, third-party components ### Implementation Checklist Copy this checklist and track progress: ``` Dark Mode Implementation: - [ ] Audit colors in codebase (grep for hex, rgb, hsl, color names) - [ ] Create semantic token layer (bg, text, border, surface, interactive, status) - [ ] Define light and dark theme values for all semantic tokens - [ ] Implement switching mechanism (data-theme attribute recommended) - [ ] Add system preference detection (prefers-color-scheme) - [ ] Add inline script in
to prevent flash of wrong theme - [ ] Build toggle component with localStorage persistence - [ ] Handle edge cases (images, shadows, third-party components, form inputs) - [ ] Test contrast ratios meet WCAG AA in both themes ``` ## Theme Token Architecture ### Layer 1: Primitive Tokens (Theme-Independent) ```css :root { /* Gray scale */ --color-gray-50: #f9fafb; --color-gray-100: #f3f4f6; --color-gray-200: #e5e7eb; --color-gray-300: #d1d5db; --color-gray-400: #9ca3af; --color-gray-500: #6b7280; --color-gray-600: #4b5563; --color-gray-700: #374151; --color-gray-800: #1f2937; --color-gray-900: #111827; --color-gray-950: #030712; /* Brand colors */ --color-blue-500: #3b82f6; --color-blue-600: #2563eb; --color-blue-400: #60a5fa; /* Semantic colors */ --color-green-500: #22c55e; --color-red-500: #ef4444; --color-amber-500: #f59e0b; } ``` ### Layer 2: Semantic Theme Tokens ```css /* Light theme (default) */ :root, [data-theme="light"] { /* Backgrounds */ --color-bg-primary: var(--color-gray-50); --color-bg-secondary: var(--color-gray-100); --color-bg-tertiary: var(--color-gray-200); --color-bg-inverse: var(--color-gray-900); /* Surfaces (cards, modals, dropdowns) */ --color-surface-primary: #ffffff; --color-surface-secondary: var(--color-gray-50); --color-surface-elevated: #ffffff; /* Text */ --color-text-primary: var(--color-gray-900); --color-text-secondary: var(--color-gray-600); --color-text-tertiary: var(--color-gray-500); --color-text-inverse: var(--color-gray-50); --color-text-disabled: var(--color-gray-400); /* Borders */ --color-border-primary: var(--color-gray-200); --color-border-secondary: var(--color-gray-300); --color-border-focus: var(--color-blue-500); /* Interactive */ --color-interactive-primary: var(--color-blue-500); --color-interactive-primary-hover: var(--color-blue-600); --color-interactive-secondary: var(--color-gray-100); --color-interactive-secondary-hover: var(--color-gray-200); /* Status */ --color-status-success: var(--color-green-500); --color-status-warning: var(--color-amber-500); --color-status-error: var(--color-red-500); --color-status-info: var(--color-blue-500); /* Shadows - more visible in light mode */ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); /* Overlay */ --color-overlay: rgb(0 0 0 / 0.5); } /* Dark theme */ [data-theme="dark"] { /* Backgrounds */ --color-bg-primary: var(--color-gray-950); --color-bg-secondary: var(--color-gray-900); --color-bg-tertiary: var(--color-gray-800); --color-bg-inverse: var(--color-gray-50); /* Surfaces - slightly elevated from background */ --color-surface-primary: var(--color-gray-900); --color-surface-secondary: var(--color-gray-800); --color-surface-elevated: var(--color-gray-800); /* Text */ --color-text-primary: var(--color-gray-50); --color-text-secondary: var(--color-gray-400); --color-text-tertiary: var(--color-gray-500); --color-text-inverse: var(--color-gray-900); --color-text-disabled: var(--color-gray-600); /* Borders - less contrast in dark mode */ --color-border-primary: var(--color-gray-800); --color-border-secondary: var(--color-gray-700); --color-border-focus: var(--color-blue-400); /* Interactive - lighter shades for dark bg */ --color-interactive-primary: var(--color-blue-500); --color-interactive-primary-hover: var(--color-blue-400); --color-interactive-secondary: var(--color-gray-800); --color-interactive-secondary-hover: var(--color-gray-700); /* Status - same or slightly adjusted */ --color-status-success: var(--color-green-500); --color-status-warning: var(--color-amber-500); --color-status-error: var(--color-red-500); --color-status-info: var(--color-blue-400); /* Shadows - less visible, add subtle glow */ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4); /* Overlay */ --color-overlay: rgb(0 0 0 / 0.7); } ``` --- ## Switching Mechanisms ### Option 1: Data Attribute (Recommended) ```html ``` ```css [data-theme="light"] { /* light tokens */ } [data-theme="dark"] { /* dark tokens */ } ``` **Pros:** Explicit, debuggable, works with SSR **Cons:** Requires JavaScript to toggle ### Option 2: CSS Class ```html ``` ```css :root { /* light tokens */ } .dark { /* dark tokens */ } ``` **Pros:** Simple, Tailwind-compatible **Cons:** Can conflict with other classes ### Option 3: Media Query Only (No Toggle) ```css :root { /* light tokens */ } @media (prefers-color-scheme: dark) { :root { /* dark tokens */ } } ``` **Pros:** Zero JavaScript, respects system **Cons:** No user override ### Option 4: Hybrid (Best UX) ```css /* Default: follow system */ :root { /* light tokens */ } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { /* dark tokens */ } } /* Manual overrides */ [data-theme="light"] { /* light tokens */ } [data-theme="dark"] { /* dark tokens */ } ``` --- ## JavaScript Implementation ### Vanilla JavaScript ```js // theme.js const STORAGE_KEY = 'theme'; function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } function getStoredTheme() { return localStorage.getItem(STORAGE_KEY); } function setTheme(theme) { const resolvedTheme = theme === 'system' ? getSystemTheme() : theme; document.documentElement.dataset.theme = resolvedTheme; localStorage.setItem(STORAGE_KEY, theme); } function initTheme() { const stored = getStoredTheme(); const theme = stored || 'system'; setTheme(theme); // Listen for system changes window.matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', (e) => { if (getStoredTheme() === 'system' || !getStoredTheme()) { document.documentElement.dataset.theme = e.matches ? 'dark' : 'light'; } }); } // Initialize on load initTheme(); // Export for toggle button window.toggleTheme = () => { const current = document.documentElement.dataset.theme; setTheme(current === 'dark' ? 'light' : 'dark'); }; ``` ### Prevent Flash of Wrong Theme (FOWT) Add inline script in `` before CSS: ```html ``` --- ## Framework Integration ### React ```tsx // ThemeProvider.tsx import { createContext, useContext, useEffect, useState } from 'react'; type Theme = 'light' | 'dark' | 'system'; interface ThemeContextValue { theme: Theme; resolvedTheme: 'light' | 'dark'; setTheme: (theme: Theme) => void; } const ThemeContext = createContext