GitHub 0

Dark Mode

Previous Next

Add light and dark themes to a sv11-ui project with mode-watcher.

sv11-ui relies on mode-watcher to toggle a .dark class on the <html> element and persist the user's choice across sessions. Every component's dark styles are scoped to that class, so wiring mode-watcher into your root layout is all that's needed for themes to work.

Install mode-watcher

npm install mode-watcher

Add the watcher

Render <ModeWatcher /> once near the top of your root layout so the class is applied and persisted before the rest of the tree renders:

<script lang="ts">
	import "../app.css";
	import { ModeWatcher } from "mode-watcher";
 
	let { children } = $props();
</script>
 
<ModeWatcher />
{@render children()}

The sv11-ui docs site renders it with defaultMode="system" and disableTransitions, which honors the user's OS preference on first load and suppresses the color-flash animation when the class flips:

<ModeWatcher defaultMode="system" disableTransitions />

See the mode-watcher documentation for the full list of props.

CSS setup

sv11-ui's Tailwind entrypoint declares a custom variant that activates whenever an element has an ancestor with the .dark class:

@custom-variant dark (&:is(.dark *));

The :root block defines the light-mode tokens, and a .dark { ... } block overrides them when the class is present. For example:

:root {
	--primary: oklch(0.205 0 0);
}
 
.dark {
	--primary: oklch(0.922 0 0);
}

Because every token is swapped in one place, you generally won't need to write your own dark: utilities — the existing components already pick up the right values automatically. If you are using a data-theme attribute or a different selector, sv11's dark styles will not activate.

Building a toggle

mode-watcher exposes a toggleMode helper that flips between light and dark. Paired with a button from the registry, a minimal toggle looks like this:

<script lang="ts">
	import { toggleMode } from "mode-watcher";
	import { Button } from "$lib/registry/ui/button";
</script>
 
<Button variant="ghost" size="icon" onclick={toggleMode}>
	<span class="sr-only">Toggle theme</span>
</Button>

This is the same pattern the sv11-ui docs site uses in its header mode switcher.

Reading the current mode

When a component needs to branch on the active theme — for example to swap a shader uniform or an image source — import the reactive mode store and read mode.current:

<script lang="ts">
	import { mode } from "mode-watcher";
 
	let isDark = $derived(mode.current === "dark");
</script>

mode.current is reactive, so anything derived from it updates automatically when the user toggles themes.