Dark mode strategies
Merid's tokens have a light and a dark value. Which one applies is decided by one attribute, data-theme, and the prefers-color-scheme media query. The token values themselves are listed in Dark mode.
1. Follow the system
Do nothing. With no data-theme on <html>, Merid follows the operating system and switches live when it changes. Add color-scheme awareness to your own CSS by using the tokens (var(--mrd-bg), var(--mrd-ink)) instead of fixed colours.
2. A toggle
Store the choice, apply it to <html> before first paint, and fall back to the system when nothing is stored.
// Inline in <head>, before any CSS-dependent paint.
const themeInitScript = `
try {
const t = localStorage.getItem("theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
} catch {}`;"use client";
import { SegmentedControl } from "@merid/react";
type Choice = "system" | "light" | "dark";
function apply(choice: Choice) {
const root = document.documentElement;
if (choice === "system") {
delete root.dataset.theme;
try { localStorage.removeItem("theme"); } catch {}
} else {
root.dataset.theme = choice;
try { localStorage.setItem("theme", choice); } catch {}
}
}
export function ThemeSwitcher() {
const [choice, setChoice] = useState<Choice>("system");
useEffect(() => {
const t = document.documentElement.dataset.theme;
setChoice(t === "light" || t === "dark" ? t : "system");
}, []);
return (
<SegmentedControl
aria-label="Theme"
value={choice}
onValueChange={(v) => { setChoice(v as Choice); apply(v as Choice); }}
options={[
{ value: "system", label: "System" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
]}
/>
);
}In Next.js put the script in the root layout's <head> with dangerouslySetInnerHTML and add suppressHydrationWarning to <html>. localStorage can throw (private mode, blocked storage), so both reads and writes are wrapped.
To sync across tabs, listen for the storage event and re-apply.
3. Per subtree
data-theme works on any element and nests in both directions: a dark panel in a light page, or a light card in a dark one.
Production
LiveTokens resolve from the nearest data-theme ancestor.
<div data-theme="dark" style={{ background: "var(--mrd-bg)", padding: 16 }}>
<Card variant="outline">…</Card>
</div>The attribute sets tokens; it does not paint. Give the element a background from the tokens so its contents sit on the right surface. Accent presets and density nest the same way — see Subtree attributes.
Portalled content (Dialog, Popover, DropdownMenu, Select, Tooltip, Toast) renders into document.body and so takes the page theme, not the subtree's. To keep an overlay in a subtree's theme, pass container (where the component supports it) pointing at an element inside the subtree.
Images and charts
- Use
<picture>withmedia="(prefers-color-scheme: dark)"only when you follow the system. With a toggle, switch sources on[data-theme="dark"]in CSS instead. - For charts, read colours from CSS variables at render time (
getComputedStyle(el).getPropertyValue("--mrd-accent")) and re-render on theme change.