# Theming & Dark Mode Design Creating effective color themes and implementing dark mode correctly. ## Dark Mode Philosophy Dark mode isn't just inverting colors—it requires deliberate design decisions to maintain usability, hierarchy, and aesthetics. ### Why Dark Mode Matters - **User preference:** Many users prefer it - **Eye strain:** Reduces strain in low-light environments - **Battery life:** Saves power on OLED screens - **Accessibility:** Some users have photosensitivity - **Professional expectation:** Users expect modern apps to support it --- ## Dark Mode Color Principles ### Don't Just Invert | Light Mode | Bad Dark Mode | Good Dark Mode | |------------|---------------|----------------| | White `#ffffff` | Black `#000000` | Dark gray `#18181b` | | Black text `#000000` | White text `#ffffff` | Off-white `#fafafa` | | Gray `#6b7280` | Gray `#6b7280` | Lighter gray `#a1a1aa` | ### Key Principles **1. Use dark grays, not pure black** Pure black (`#000000`) creates harsh contrast and "halation" (text appears to glow). ```css /* Background scale for dark mode */ --bg-base: #09090b; /* Deepest background */ --bg-subtle: #18181b; /* Cards, elevated surfaces */ --bg-muted: #27272a; /* Hover states, inputs */ --bg-emphasis: #3f3f46; /* Active states */ ``` **2. Reduce contrast slightly** Max contrast in dark mode is harsher than in light mode. ```css /* Text colors for dark mode */ --text-primary: #fafafa; /* ~95% white, not 100% */ --text-secondary: #a1a1aa; /* Muted text */ --text-tertiary: #71717a; /* Subtle text */ ``` **3. Desaturate colors** Bright saturated colors on dark backgrounds cause eye strain. ```css /* Light mode brand color */ --primary-light: #3b82f6; /* Bright blue */ /* Dark mode - slightly desaturated */ --primary-dark: #60a5fa; /* Lighter, less saturated */ ``` **4. Elevate with lightness, not shadow** In dark mode, shadows are invisible. Show elevation with lighter surfaces. ```css /* Light mode: shadow for depth */ .card-light { background: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } /* Dark mode: lighter surface for depth */ .card-dark { background: #27272a; /* Lighter than base */ box-shadow: none; /* Or very subtle */ } ``` --- ## Building a Dark Mode Palette ### Step 1: Define Your Gray Scale Create 9-10 shades from near-black to near-white: ```css /* Dark mode gray scale (Zinc example) */ --gray-950: #09090b; /* Deepest background */ --gray-900: #18181b; /* Card backgrounds */ --gray-800: #27272a; /* Elevated surfaces */ --gray-700: #3f3f46; /* Borders, dividers */ --gray-600: #52525b; /* Disabled states */ --gray-500: #71717a; /* Placeholder text */ --gray-400: #a1a1aa; /* Secondary text */ --gray-300: #d4d4d8; /* Primary text (alt) */ --gray-200: #e4e4e7; /* Headings */ --gray-100: #f4f4f5; /* Emphasis text */ --gray-50: #fafafa; /* Primary text */ ``` ### Step 2: Adjust Accent Colors ```css /* Primary color adjustments */ /* Light mode: use 500-600 range */ --primary-light: #2563eb; /* Dark mode: use 400-500 range (lighter) */ --primary-dark: #3b82f6; /* Same for semantic colors */ --success-light: #16a34a; --success-dark: #22c55e; --error-light: #dc2626; --error-dark: #ef4444; ``` ### Step 3: Define Semantic Tokens ```css /* Semantic tokens that switch based on mode */ :root { --color-bg: var(--gray-50); --color-bg-subtle: var(--gray-100); --color-text: var(--gray-900); --color-text-muted: var(--gray-600); --color-border: var(--gray-200); --color-primary: var(--blue-600); } [data-theme="dark"] { --color-bg: var(--gray-950); --color-bg-subtle: var(--gray-900); --color-text: var(--gray-50); --color-text-muted: var(--gray-400); --color-border: var(--gray-800); --color-primary: var(--blue-400); } ``` --- ## Implementation Strategies ### Strategy 1: CSS Custom Properties ```css :root { --bg: #ffffff; --text: #18181b; } @media (prefers-color-scheme: dark) { :root { --bg: #18181b; --text: #fafafa; } } body { background: var(--bg); color: var(--text); } ``` ### Strategy 2: Data Attribute + Class ```html ``` ```css [data-theme="light"] { --bg: #ffffff; } [data-theme="dark"] { --bg: #18181b; } ``` ```javascript // Toggle theme function toggleTheme() { const current = document.documentElement.dataset.theme; document.documentElement.dataset.theme = current === 'dark' ? 'light' : 'dark'; localStorage.setItem('theme', document.documentElement.dataset.theme); } // Initialize from preference function initTheme() { const saved = localStorage.getItem('theme'); const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; document.documentElement.dataset.theme = saved || preferred; } ``` ### Strategy 3: Tailwind Dark Mode ```html ``` ```javascript // tailwind.config.js module.exports = { darkMode: 'class', // or 'media' for system preference only } ``` --- ## Component Considerations ### Cards and Surfaces ```css /* Light: white with shadow */ .card { background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } /* Dark: lighter surface, subtle or no shadow */ [data-theme="dark"] .card { background: var(--gray-900); box-shadow: 0 1px 3px rgba(0,0,0,0.3); /* Darker shadow if any */ /* Or: border: 1px solid var(--gray-800); */ } ``` ### Form Inputs ```css .input { background: white; border: 1px solid var(--gray-300); } [data-theme="dark"] .input { background: var(--gray-900); border: 1px solid var(--gray-700); } ``` ### Buttons ```css /* Primary button */ .btn-primary { background: var(--primary); color: white; } [data-theme="dark"] .btn-primary { /* Often same or slightly adjusted */ background: var(--primary-dark); } /* Secondary button */ .btn-secondary { background: var(--gray-100); color: var(--gray-900); } [data-theme="dark"] .btn-secondary { background: var(--gray-800); color: var(--gray-100); } ``` ### Images and Media ```css /* Reduce brightness/contrast of images in dark mode */ [data-theme="dark"] img:not([data-no-dim]) { filter: brightness(0.9) contrast(1.1); } /* Invert diagrams/illustrations if needed */ [data-theme="dark"] .diagram { filter: invert(1) hue-rotate(180deg); } ``` ### Syntax Highlighting Don't forget code blocks need dark mode variants: - Use dark theme variants of syntax highlighters - Or invert colors appropriately - Popular: One Dark, Dracula, Night Owl --- ## Testing Dark Mode ### Checklist - [ ] All text is readable (sufficient contrast) - [ ] Hierarchy still clear (headings vs body) - [ ] Focus states visible - [ ] Images don't blow out - [ ] Forms inputs clearly visible - [ ] Error/success states distinct - [ ] Loading states visible - [ ] Shadows/elevation still work - [ ] Icons visible (may need color swap) - [ ] Brand colors still recognizable ### Contrast Ratios Same WCAG requirements apply: - Normal text: 4.5:1 minimum - Large text: 3:1 minimum - UI components: 3:1 minimum **Common dark mode fails:** - Gray text on dark background - Colored text on colored backgrounds - Disabled states too subtle --- ## Theme Toggle UI ### Placement - Header/navigation (most common) - Settings page - Footer (less common) ### Icon Patterns ```html ``` ### State Options 1. **Light / Dark** - Simple toggle 2. **Light / Dark / System** - Respect OS preference option 3. **Auto only** - Always follow system (no toggle) ### Persistence ```javascript // Save preference localStorage.setItem('theme', 'dark'); // Load preference (with system fallback) const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); ``` --- ## Advanced: Multiple Themes ### Brand Themes ```css [data-theme="brand-a"] { --primary: #ff6b6b; --primary-hover: #ee5a5a; } [data-theme="brand-b"] { --primary: #4ecdc4; --primary-hover: #3dbdb5; } ``` ### Theme Structure ```css /* Base tokens (don't change) */ --spacing-4: 16px; --radius-md: 8px; /* Color tokens (change per theme) */ --color-primary: ...; --color-bg: ...; /* Component tokens (reference color tokens) */ --button-bg: var(--color-primary); --card-bg: var(--color-bg); ``` --- ## Common Mistakes | Mistake | Problem | Fix | |---------|---------|-----| | Pure black background | Harsh, looks flat | Use dark gray (#18181b) | | Pure white text | Too much contrast | Use off-white (#fafafa) | | Same saturated colors | Eye strain | Desaturate for dark mode | | Shadows for elevation | Invisible in dark | Use lighter surfaces | | Forgetting images | Can be too bright | Dim images slightly | | One contrast check | Colors interact differently | Check all combinations | | Forgetting focus states | Invisible borders | Ensure visible focus rings | --- ## Quick Reference ### Minimum Viable Dark Mode ```css :root { --bg: #ffffff; --bg-subtle: #f4f4f5; --text: #18181b; --text-muted: #71717a; --border: #e4e4e7; --primary: #2563eb; } @media (prefers-color-scheme: dark) { :root { --bg: #18181b; --bg-subtle: #27272a; --text: #fafafa; --text-muted: #a1a1aa; --border: #3f3f46; --primary: #3b82f6; } } body { background: var(--bg); color: var(--text); } ```