Introduction
Dark mode for a site — the visitor's choice, the system preference, and the class on the html element.
Good afternoon
Every color here follows the manager's mode.
rendering … · your system prefers …
import { Div, P, Span } from "@implementjs/core";
import { MonitorIcon, MoonIcon, SunIcon } from "@implementjs/lucide";
import { createModeManager, type Mode } from "@implementjs/mode-watcher";
import { Button } from "@/lib/components/ui/button";
// A real app mounts `ModeWatcher({ manager })` at the root and lets the manager
// put the class on `<html>`. This demo only reads the manager, so the preview
// below changes instead of the page around it.
const manager = createModeManager({ modeStorageKey: "docs-mode-demo" });
const choices = [
{ mode: "light", label: "Light", icon: SunIcon },
{ mode: "system", label: "System", icon: MonitorIcon },
{ mode: "dark", label: "Dark", icon: MoonIcon },
] satisfies { mode: Mode; label: string; icon: typeof SunIcon }[];
export default function ModeWatcherDemo() {
return Div(
{ class: "flex w-full max-w-sm flex-col gap-4" },
Div(
{ class: "flex items-center justify-center gap-2" },
...choices.map(({ mode, label, icon: Icon }) =>
Button(
{
variant: "outline",
size: "sm",
"aria-pressed": manager.userPrefersMode.bind((current) => current === mode),
class: manager.userPrefersMode.bind((current) =>
current === mode ? "bg-accent text-accent-foreground" : "",
),
onClick: () => manager.setMode(mode),
},
Icon({ "aria-hidden": true }),
label,
),
),
),
Div(
{
class: [
"flex flex-col gap-3 rounded-lg border p-4 transition-colors",
manager.mode.bind((mode) =>
mode === "light"
? "border-neutral-200 bg-white text-neutral-900"
: "border-neutral-800 bg-neutral-950 text-neutral-50",
),
],
},
P({ class: "text-sm font-medium" }, "Good afternoon"),
P(
{
class: [
"text-sm",
manager.mode.bind((mode) =>
mode === "light" ? "text-neutral-500" : "text-neutral-400",
),
],
},
"Every color here follows the manager's mode.",
),
Div(
{ class: "flex gap-2" },
...["bg-sky-500", "bg-emerald-500", "bg-amber-500"].map((color) =>
Div({ class: ["size-6 rounded-full", color] }),
),
),
),
P(
{ class: "text-center text-xs text-muted-foreground" },
"rendering ",
Span(
{ class: "font-medium" },
manager.mode.bind((mode) => mode ?? "…"),
),
" · your system prefers ",
Span(
{ class: "font-medium" },
manager.systemPrefersMode.bind((mode) => mode ?? "…"),
),
),
);
}Dark mode is three questions: what did the visitor pick, what does their operating system prefer, and which of the two is the page rendering right now. @implementjs/mode-watcher answers all three. It is a port of mode-watcher — a manager owns the mode, and a component mounts once at the root and keeps <html> in step with it.
Add it next to core:
npm install @implementjs/core @implementjs/mode-watcher
import { App, Button } from "@implementjs/core";
import { createModeManager, ModeWatcher } from "@implementjs/mode-watcher";
export const mode = createModeManager();
App({ target: document.body }).render(
ModeWatcher({ manager: mode }),
Button({ onClick: () => mode.toggleMode() }, "Toggle theme"),
);
This site runs on it — the switcher in the header sets the mode, and everything below <html> follows. That is the whole setup. <html> gets class="dark" and style="color-scheme: dark" when the mode is dark, the choice is remembered in localStorage, and the page comes back in the same mode next visit — without the flash of the wrong theme that usually comes with it.
In a kit app, ModeWatcher goes in the root layout so it stays mounted across navigations:
// src/routes/layout.ts
import { Div, Main } from "@implementjs/core";
import { ModeWatcher } from "@implementjs/mode-watcher";
import { mode } from "@/lib/mode";
import type { LayoutProps } from "./$types";
export default function Layout({ children }: LayoutProps) {
return Div(ModeWatcher({ manager: mode }), Main(children));
}
create-implement-app can do all of this for you — pick the @implementjs/mode-watcher addon and the app it scaffolds opens on a working light/dark toggle.
Where to next
- The manager covers what owns the mode, and how to read and change it.
- Styling covers the classes on
<html>, themes, and transitions. - First paint covers the blocking script that beats the flash.
- API lists every export.