Implement a Global Theme Toggle in Svelte with SSR‑Safe Persistence
Learn how to add a global, SSR‑safe theme toggle to a Svelte app. Use a writable store, persist to localStorage on the client, and keep the UI reactive across all components.
13 Aug 2026, 08:04 UTC

Desired Outcome
Build a single, globally‑available theme toggle that:
- Switches styles instantly across all components.
- Persists the user’s choice in
localStorageso the preference survives page reloads. - Does not break server‑side rendering (SSR) – the app must render a default theme on the server and hydrate correctly on the client.
Prerequisites
- Node.js 20+ and npm or yarn installed.
- A SvelteKit project (or a plain Svelte app with a build step that supports SSR).
- Basic knowledge of Svelte stores and the
onMountlifecycle hook.
Setting Up the Theme Store
Create a dedicated store that holds the current theme string. It should expose the standard set, update, and subscribe methods.
/// src/stores/theme.js
import { writable, derived } from 'svelte/store';
// 1. Base store – holds the raw theme value.
export const themeStore = writable('light');
// 2. Derived store – exposes a CSS class name for convenience.
export const themeClass = derived(themeStore, ($themeStore) =>
$themeStore === 'dark' ? 'theme-dark' : 'theme-light'
);
Using a derived store keeps component templates clean: you can simply bind to $themeClass instead of writing conditional logic.
Persisting to localStorage Safely
SSR environments do not have a window object. All localStorage interactions must be guarded with typeof window !== 'undefined' and executed only after the component mounts on the client.
/// src/hooks.client.js (SvelteKit) or any client‑only module
import { onMount } from 'svelte';
import { themeStore } from './stores/theme.js';
onMount(() => {
if (typeof window === 'undefined') return;
const stored = localStorage.getItem('app-theme');
if (stored && (stored === 'light' || stored === 'dark')) {
themeStore.set(stored);
}
// Persist every change
const unsubscribe = themeStore.subscribe((value) => {
localStorage.setItem('app-theme', value);
});
return unsubscribe; // cleanup on component unmount
});
Running this code only on the client guarantees that SSR never tries to read or write localStorage.
Toggling in Components
Expose a simple button that flips the theme. Because the store is reactive, every subscriber updates automatically.
/// src/lib/ThemeToggle.svelte
Toggle Theme
In your root layout or top‑level component, bind the derived class to the class attribute:
/// src/routes/+layout.svelte (SvelteKit)
Now the <div> will always carry either theme-light or theme-dark, letting CSS switch styles.
SSR Considerations
- Initialize
themeStorewith the default theme (e.g.,'light') before the app renders on the server. - Do not attempt to read
localStorageon the server; defer that toonMountas shown above. - During hydration, Svelte will reconcile the server‑rendered markup with the client‑side store. Because the store starts with the same default value, no visual flash occurs.
Testing the Feature
- Run the dev server:
npm run devand open the app. Click the toggle button; the background color should switch immediately. - Refresh the page: The chosen theme should persist, confirming
localStorageintegration. - Inspect the generated HTML: Build the app (
npm run build) and openbuild/index.html. The root<div>should contain the defaulttheme-lightclass, showing SSR worked. - Console check: In the browser console, run
import { themeStore } from './stores/theme.js'; themeStore.subscribe(console.log);to see live updates as you toggle.
Recovery Options
- If the toggle stops working, ensure the
onMountguard is present and thatlocalStorageaccess is not executed during SSR. - Verify that
themeStoreis exported from a single module and imported consistently across the app to avoid duplicate instances. - Check the console for errors like
ReferenceError: localStorage is not defined– this indicates SSR code is leaking into the server bundle. - When debugging, temporarily log the store value after each update:
themeStore.subscribe(v => console.log('Theme:', v));to confirm state changes propagate.
Limitations & Tips
- Only two themes are shown; for more options, store an object with theme metadata and expose derived values for class names.
- Large theme objects can trigger many re‑renders; keep the store payload minimal (just the theme key).
- Derived stores must not depend on each other in a circular way – otherwise Svelte will throw an update loop error.
- Unsubscribe from stores manually only if you use the raw
subscribemethod; the$syntax handles cleanup automatically.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.