Dark mode
One attribute on the html element switches every theme between light and dark.
How it works
Set data-theme="dark" on the html element. Every colour token switches to its dark value and the components follow, including their animations.
<html lang="en" data-theme="dark">Every theme ships both sets of tokens in the same file, so there is nothing extra to install. A dark class on html works too.
Add a toggle
The theme only reads the attribute. Remembering the choice is up to your app. This button saves it in localStorage.
import { useEffect, useState } from "react";
import { Button } from "./components/ui/button";
export function ThemeToggle() {
const [mode, setMode] = useState("light");
useEffect(() => {
setMode(document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
}, []);
function toggle() {
const next = mode === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("mode", next);
setMode(next);
}
return (
<Button variant="outline" onClick={toggle}>
{mode === "dark" ? "Switch to light" : "Switch to dark"}
</Button>
);
}Follow the system setting
Put a small script in the head of your page. It uses the saved choice, or the visitor's system setting when nothing is saved, and runs before the first paint so the page never flashes the wrong colours.
<script>
var mode = localStorage.getItem("mode");
if (!mode) mode = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", mode);
</script>Try it
This changes the whole site, not only the box.
html element now has data-theme="light".import { useEffect, useState } from "react";
import { Button } from "./components/ui/button";
export function ThemeToggle() {
const [mode, setMode] = useState("light");
useEffect(() => {
setMode(document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
}, []);
function toggle() {
const next = mode === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("mode", next);
setMode(next);
}
return (
<Button variant="outline" onClick={toggle}>
{mode === "dark" ? "Switch to light" : "Switch to dark"}
</Button>
);
}Dark styles in your own code
Use the semantic classes, such as bg-surface and text-fg-primary. They change by themselves, so most of your code needs no dark rules.
When you do need one, the Tailwind dark: variant is wired to the same attribute.
<div className="bg-surface dark:bg-subtle">…</div>