3.9 KiB
3.9 KiB
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:
/* 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
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:
function getStorage() {
try {
localStorage.setItem('__test__', '1');
localStorage.removeItem('__test__');
return localStorage;
} catch {
return sessionStorage; // Fallback
}
}
Why It Works
- System mode: No
data-themeattribute → media query applies - Light mode:
data-theme="light"→ overrides media query - Dark mode:
data-theme="dark"→ overrides media query
Firefox Fix
Problem: Initial implementation had invalid CSS:
/* ❌ Invalid */
html[data-theme="dark"],
@media (prefers-color-scheme: dark) { ... }
Solution: Separate rules:
/* ✅ 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:
- auto - Simple default theme
- nord - Nord color palette (light/dark variants)
- gruvbox - Gruvbox color palette (light/dark variants)
- catppuccin-latte - Catppuccin Latte → Mocha (light → dark)
- catppuccin-frappe - Catppuccin Latte → Frappé (light → dark)
- catppuccin-macchiato - Catppuccin Latte → Macchiato (light → dark)
- 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 scriptthemes/*.css- Theme CSS files with light/dark variantsinternal/config/config.go- Auto-discovers available themes
Status: ✅ Working in all browsers Last Updated: 2025-10-12