Foundations
Dark mode
A dark class on the html element switches every component. Three pieces make it production quality: apply the stored choice before paint, offer a toggle, and follow the system setting by default.
This is the shipped ThemeToggle. It switches this page and remembers the choice.
- The theme's
.darkblock holds every dark value. Adding thedarkclass to<html>switches them all at once. - The choice is stored in
localStorageunderthemeaslight,dark, orsystem. With nothing stored, the operating system decides. While a toggle oruseThemeis on the page, a change to the system setting is followed immediately. - Other open tabs follow a change, and so does every component using the hook.
Add the components
npx aback-ui@latest add theme-toggleWith the npm package,
ThemeScript,ThemeToggle, anduseThemeare already exported from@aback-ui/ui.Apply the stored theme before paint
Render
ThemeScriptin the root layout. It is a tiny inline script that sets the class before the first paint, so a dark-mode visitor never sees a white flash.app/layout.tsxtsx import { ThemeScript } from "@/components/ui/theme-toggle"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <ThemeScript /> </head> <body>{children}</body> </html> ); }suppressHydrationWarningis needed because the script changes thehtmlclass before React hydrates. Passnoncewhen your Content Security Policy requires one.Add a toggle
components/site-header.tsxtsx import { ThemeToggle } from "@/components/ui/theme-toggle"; export function SiteHeader() { return ( <header className="flex h-16 items-center justify-between border-b border-separator px-4"> <a href="/">Home</a> <ThemeToggle /> </header> ); }It is an icon button with an accessible name that says what it will do, such as "Switch to dark theme". Rename it with
toDarkLabelandtoLightLabel.
Build your own control
useTheme reads and writes the same stored choice, for a menu or a settings page.
"use client";
import { useTheme } from "@/components/ui/theme-toggle";
export function ThemeSelect() {
const { theme, setTheme } = useTheme();
return (
<select value={theme} onChange={(event) => setTheme(event.target.value as typeof theme)}>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
);
}theme is the stored choice, including system. resolvedTheme is what is on screen. During the server render it is light, so read it only for things that can change after hydration.
Nothing here depends on Next.js. In Vite or any single-page app, put the ThemeScript markup in index.html, or render the component before your app root. Prefer a different storage key? Pass the same storageKey to ThemeScript, ThemeToggle, and useTheme.
The colours themselves are covered in Theming.