svelte-theme-select
Version:
Customizable Svelte components for theme selection (light mode / dark mode) inspired by TailwindCSS. Flicker-free, synchronizes across tabs, works with or without SSR and doesn't require unnecessary use of `transformPageChunk` so is cache-friendly.
68 lines (67 loc) • 2.08 kB
JavaScript
import { MediaQuery } from "svelte/reactivity";
import { on } from "svelte/events";
import { BROWSER } from "esm-env";
export const Theme = ['light', 'dark', 'system'];
class ThemeState {
#mq = new MediaQuery('(prefers-color-scheme: dark)');
#system = $derived(this.#mq.current ? 'dark' : 'light');
#override = $state('system');
#value = $derived(this.#override === 'system' ? this.#system : this.#override);
#subscribers = 0;
#off;
constructor() {
if (BROWSER) {
const saved = localStorage.theme ?? 'system';
this.#override = saved;
}
}
subscribe() {
if ($effect.tracking()) {
$effect(() => {
if (this.#subscribers === 0) {
this.#off = on(window, 'storage', (event) => {
if (event.key === 'theme') {
this.#override = event.newValue;
}
});
$effect(() => {
document.documentElement.classList.toggle('dark', this.#value === 'dark');
});
}
this.#subscribers++;
return () => {
this.#subscribers--;
if (this.#subscribers === 0) {
this.#off?.();
this.#off = undefined;
}
};
});
}
}
get system() {
this.subscribe();
return this.#system;
}
get override() {
this.subscribe();
return this.#override;
}
set override(value) {
switch (value) {
case 'dark':
case 'light':
localStorage.setItem('theme', value);
break;
case 'system':
localStorage.removeItem('theme');
break;
}
this.#override = value;
}
get current() {
this.subscribe();
return this.#value;
}
}
export const theme = new ThemeState();