Files
linkbeam/HTML Theme Switcher Implementation.md

163 lines
3.9 KiB
Markdown

## 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