@dazl/color-scheme
Version:
Color scheme utilities and tools
102 lines (94 loc) • 3.35 kB
text/typescript
import type {
ColorSchemeSetup,
ColorSchemeApi,
ColorSchemeSubscriber,
ColorSchemeConfig,
ColorSchemeResolve,
} from './types';
function initiateColorScheme({ saveConfig, loadConfig, cssClass }: ColorSchemeSetup): ColorSchemeApi {
const state: {
listeners: Set<ColorSchemeSubscriber>;
config: ColorSchemeConfig;
} = {
listeners: new Set<ColorSchemeSubscriber>(),
config: loadConfig(),
};
const isDarkQuery = window.matchMedia('(prefers-color-scheme: dark)');
const resolveSystem = (): ColorSchemeResolve => (isDarkQuery.matches ? 'dark' : 'light');
const onSystemChange = (): void => {
if (state.config !== 'system') return;
updateDocument();
};
const currentState = () => {
const config = state.config;
const resolved = config === 'system' ? resolveSystem() : config;
return { config, resolved };
};
const updateDocument = (): void => {
const current = currentState();
const root = document.documentElement;
root.classList.remove(cssClass.light, cssClass.dark);
root.classList.add(cssClass[current.resolved]);
root.style.colorScheme = current.resolved === 'dark' ? 'dark' : 'light';
state.listeners.forEach((listener) => listener(current));
};
// set initial color scheme and listen for system changes
updateDocument();
isDarkQuery.addEventListener('change', onSystemChange);
return {
get config() {
return state.config;
},
set config(config) {
if (config === state.config) return;
state.config = config;
updateDocument();
saveConfig(config);
},
get currentState() {
return currentState();
},
get resolvedSystem(): ColorSchemeResolve {
return resolveSystem();
},
getRootCssClass(resolved = currentState().resolved) {
return cssClass[resolved];
},
subscribe: (sub: ColorSchemeSubscriber): (() => void) => {
state.listeners.add(sub);
return (): void => {
state.listeners.delete(sub);
};
},
dispose: () => {
state.listeners.clear();
isDarkQuery.removeEventListener('change', onSystemChange);
},
};
}
const storageKey: string = 'color-scheme';
const scriptDataset = document.currentScript?.dataset;
const darkCssClass = scriptDataset?.darkClass || 'dark-theme';
const lightCssClass = scriptDataset?.lightClass || 'light-theme';
window.colorSchemeApi ??= initiateColorScheme({
loadConfig(): ColorSchemeConfig {
try {
const config: string | null = localStorage.getItem(storageKey);
return config === 'light' || config === 'dark' ? config : 'system';
} catch {
return 'system';
}
},
saveConfig(config: ColorSchemeConfig): void {
try {
if (config === 'system') {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, config);
}
} catch {
// Fallback to no-op if localStorage is not available
}
},
cssClass: { light: lightCssClass, dark: darkCssClass },
});