Most websites nowadays have light mode, dark mode, and a system adaptive mode. It’s pretty trivial to implement this in Svelte.
In this post I will use my personal website as an example to show how to implement this feature.
Demo
Implementation
Warning
This implementation works, but it has an issue: when loading into the page with system theme set to dark, the page flashes white for a second before switching to dark mode.
// stores.ts
import{writable,typeWritable}from'svelte/store';// Possible theme options
exporttypeThemeType='light'|'dark'|'auto';// Preference store: any other preference can be added here
exporttypePrefType={theme: ThemeType;}// Store type: this is the object that will be stored in localstorage
exporttypeStoreType={pref: PrefType;}// Initialize the store
conststored=localStorage.content;conststore: Writable<StoreType>=writable(stored?JSON.parse(stored):{pref:{theme:'auto',}});store.subscribe(value=>{localStorage.content=JSON.stringify(value);});exportdefaultstore;
Svelte component
Finally, create a Svelte component to let user select the color theme.
The gist is to use prefers-color-schememedia query to detect the user’s system preference. If the user selects a specific theme from dropdown, it will override the system preference.
Info
For the dropdown component, I created my own with i18n support. The code is available here, but any dropdown component will work.
<!-- Preference.svelte --><scriptlang="ts">importCardfrom"$lib/Card.svelte";import{onDestroy}from"svelte";importstore,{typeThemeType}from"./stores";importDropdownfrom"./Dropdown.svelte";let{onConfirm=()=>{}}:{onConfirm?:()=>void;}=$props();constswitchColor=(dark: boolean)=>{if(dark){document.body.classList.add('dark');}else{document.body.classList.remove('dark');}};constmediaQuery=window.matchMedia('(prefers-color-scheme: dark)');letswitchFn:((e: MediaQueryListEvent)=>void)|null=null;constswitchTheme=(theme: ThemeType)=>{if(theme==='auto'){// Add the listener only if it doesn't exist
if(!switchFn){switchFn=(e: MediaQueryListEvent)=>switchColor(e.matches);mediaQuery.addEventListener('change',switchFn);}// Set the initial color based on current preference
switchColor(mediaQuery.matches);return;}// Remove the listener if switching to a specific theme
if(switchFn){mediaQuery.removeEventListener('change',switchFn);switchFn=null;}// Manually set the theme
switchColor(theme==='dark');};// Subscribe to store changes
constunsubscribe=store.subscribe(store=>{// theme
consttheme=store.pref.theme;switchTheme(theme);});// Cleanup on component destroy
onDestroy(()=>{if(switchFn){mediaQuery.removeEventListener('change',switchFn);}unsubscribe();});// Theme options
typethemeOptType={value: ThemeType,txLabel: string};letthemeOptions: themeOptType[]=[{value:'light',txLabel:'pref.colorTheme.light'},{value:'dark',txLabel:'pref.colorTheme.dark'},{value:'auto',txLabel:'pref.colorTheme.auto'},];</script><!--...--><Dropdownbind:selected={$store.pref.theme}options={themeOptions}onChange={onConfirm}/><!--...-->