jonnybarnes.uk/resources/js/theme-selector.js
Jonny Barnes c3cf597950
Persist the theme selector choice across page loads
Save an explicit light/dark choice to localStorage and clear it when
switching back to system. An inline head script applies the saved
colour scheme before first paint to avoid a flash of the system theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:23:02 +01:00

88 lines
2.6 KiB
JavaScript

class ThemeSelector {
constructor() {
this.widget = document.querySelector('#theme-selector');
this.btnLight = this.widget.querySelector('#btn-light');
this.btnDark = this.widget.querySelector('#btn-dark');
this.status = this.widget.querySelector('#theme-status');
this.currentTheme = ThemeSelector.storedTheme();
this.updateButtons();
}
static storedTheme() {
try {
const theme = localStorage.getItem('theme');
return (theme === 'light' || theme === 'dark') ? theme : 'system';
} catch {
return 'system';
}
}
static storeTheme(theme) {
try {
if (theme === 'system') {
localStorage.removeItem('theme');
} else {
localStorage.setItem('theme', theme);
}
} catch {
/* storage unavailable, the choice just won't persist */
}
}
updateButtons() {
this.btnLight.setAttribute('aria-pressed', String(this.currentTheme === 'light'));
this.btnDark.setAttribute('aria-pressed', String(this.currentTheme === 'dark'));
}
setupEventListeners() {
this.btnLight.addEventListener('click', () => {
this.applyTheme(this.currentTheme === 'light' ? 'system' : 'light');
});
this.btnDark.addEventListener('click', () => {
this.applyTheme(this.currentTheme === 'dark' ? 'system' : 'dark');
});
}
applyTheme(theme) {
const selectedTheme = (theme === 'light' || theme === 'dark') ? theme : 'light dark';
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
const html = document.documentElement;
let currentColorScheme = html.style.getPropertyValue('color-scheme');
if (currentColorScheme === '') {
currentColorScheme = 'light dark';
}
/* do we need to transition */
let doTransition = false;
if (selectedTheme !== currentColorScheme) {
if (selectedTheme === 'light dark') {
doTransition = currentColorScheme !== systemTheme;
} else if (currentColorScheme === 'light dark') {
doTransition = selectedTheme !== systemTheme;
} else {
doTransition = true;
}
}
const applyChange = () => {
this.currentTheme = theme;
html.style.setProperty('color-scheme', selectedTheme);
ThemeSelector.storeTheme(theme);
this.updateButtons();
this.status.textContent = theme === 'system' ? 'Theme set to system default' : '';
};
html.style.viewTransitionName = 'changing-theme';
if (doTransition && document.startViewTransition) {
document.startViewTransition(applyChange);
} else {
applyChange();
}
}
}
export { ThemeSelector };