July 21, 2026 · 1 min read
"Keeping every tab in sync: multi-window theming without a backend"
"How Theme Kit synchronizes theme changes across tabs and windows using BroadcastChannel, SharedWorker and StorageEvent — with a graceful cascade of fallbacks."
When a user flips to dark mode in one tab, every other open tab of your app should follow instantly — no refresh, no polling, no backend. Theme Kit ships three transport strategies and a cascade that picks the best one available.
The cascade
createMultiWindowSync() probes the environment and selects the first transport
that works:
- BroadcastChannel — the modern, dedicated channel for same-origin messaging. Zero setup, instant delivery.
- SharedWorker — a shared thread all tabs talk to. Useful when you also want cross-tab coordination beyond theming.
- StorageEvent — the classic
localStorage+storageevent pair. Works everywhere, including older browsers.
1import { createThemeRuntime, createMultiWindowSync } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({ themes });
4const sync = createMultiWindowSync({
5 channelName: "my-app-themes",
6});
7
8// Wire incoming selections into the runtime
9sync.subscribe((selection) => {
10 runtime.selection.setMode(selection.mode);
11 runtime.selection.setFamily(selection.family);
12});
13
14// Broadcast a local change to every other tab
15sync.post({ mode: "dark", family: "slate" });Each selection change is broadcast to the other windows, which apply it
locally. The createThemeSelectionBroadcast() adapter does the same for raw
selection updates when you only care about family + mode.
Why this matters
Theme state is global UI state. If one tab shows light and another shows dark, your app feels broken. Multi-window sync turns theming into a true single-user-interface across the whole browsing session — and it's the same pattern the docs site's Playground demo shows with a real second tab.
Caveats
- BroadcastChannel and SharedWorker require secure contexts (HTTPS or
localhost). - StorageEvent fires in other tabs, not the one that wrote — which is exactly what we want.
- Keep the channel name stable across your app; a mismatch silently disables sync.