## What It Does A three-option theme toggle (System/Light/Dark) that works reliably across all browsers. **Features:** - Cycles through: System (πŸ’‘) β†’ Light (β˜€οΈ) β†’ Dark (πŸŒ™) - Persists across page refreshes - Follows OS theme preference when "System" is selected - Works in private browsing mode - Mobile-friendly (icon-only) ## The Solution ### CSS Three separate rules that work together: ```css /* Default: Light mode */ :root { --bg-primary: var(--nord-snow-storm-3); --text-primary: var(--nord-polar-night-2); /* ... */ } /* System dark mode */ @media (prefers-color-scheme: dark) { :root { --bg-primary: var(--nord-polar-night-1); --text-primary: var(--nord-snow-storm-3); /* ... */ } } /* Manual overrides (highest priority) */ html[data-theme="dark"] { --bg-primary: var(--nord-polar-night-1); --text-primary: var(--nord-snow-storm-3); /* ... */ } html[data-theme="light"] { --bg-primary: var(--nord-snow-storm-3); --text-primary: var(--nord-polar-night-2); /* ... */ } ``` **Why this works:** `html[data-theme]` has higher specificity than the media query, so manual selection always overrides system preference. ### JavaScript ```javascript const THEME_OPTIONS = ['system', 'light', 'dark']; function toggleTheme() { const current = getFromStorage('themePreference', 'system'); const nextIndex = (THEME_OPTIONS.indexOf(current) + 1) % 3; const next = THEME_OPTIONS[nextIndex]; applyTheme(next); } function applyTheme(theme) { // Update HTML attribute if (theme === 'system') { document.documentElement.removeAttribute('data-theme'); } else { document.documentElement.setAttribute('data-theme', theme); } // Update UI document.getElementById('themeIcon').textContent = THEME_ICONS[theme]; document.getElementById('themeLabel').textContent = THEME_LABELS[theme]; // Save preference saveToStorage('themePreference', theme); } // Initialize on load initTheme(); ``` ### Storage Fallback Works even when localStorage is blocked: ```javascript function getStorage() { try { localStorage.setItem('__test__', '1'); localStorage.removeItem('__test__'); return localStorage; } catch { return sessionStorage; // Fallback } } ``` ## Why It Works 1. **System mode**: No `data-theme` attribute β†’ media query applies 2. **Light mode**: `data-theme="light"` β†’ overrides media query 3. **Dark mode**: `data-theme="dark"` β†’ overrides media query ## Firefox Fix **Problem:** Initial implementation had invalid CSS: ```css /* ❌ Invalid */ html[data-theme="dark"], @media (prefers-color-scheme: dark) { ... } ``` **Solution:** Separate rules: ```css /* βœ… Valid */ @media (prefers-color-scheme: dark) { :root { ... } } html[data-theme="dark"] { ... } ``` ## Tests 12 unit tests verify: - Theme storage/retrieval - localStorage β†’ sessionStorage fallback - All three theme options work Run: `npm test` ## Available Themes LinkBeam includes the following themes with automatic light/dark switching: 1. **auto** - Simple default theme 2. **nord** - Nord color palette (light/dark variants) 3. **gruvbox** - Gruvbox color palette (light/dark variants) 4. **catppuccin-latte** - Catppuccin Latte β†’ Mocha (light β†’ dark) 5. **catppuccin-frappe** - Catppuccin Latte β†’ FrappΓ© (light β†’ dark) 6. **catppuccin-macchiato** - Catppuccin Latte β†’ Macchiato (light β†’ dark) 7. **catppuccin-mocha** - Catppuccin Latte β†’ Mocha (light β†’ dark) All themes follow the same CSS structure with: - Default light mode colors in `:root` - Dark mode colors in `@media (prefers-color-scheme: dark)` - Manual overrides via `html[data-theme="light|dark"]` ## Files - `templates/base.html` - Theme switcher button and script - `themes/*.css` - Theme CSS files with light/dark variants - `internal/config/config.go` - Auto-discovers available themes --- **Status:** βœ… Working in all browsers **Last Updated:** 2025-10-12