Skip to content
Aback UI

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 .dark block holds every dark value. Adding the dark class to <html> switches them all at once.
  • The choice is stored in localStorage under theme as light, dark, or system. With nothing stored, the operating system decides. While a toggle or useTheme is 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.
  1. Add the components

    npx aback-ui@latest add theme-toggle

    With the npm package, ThemeScript, ThemeToggle, and useTheme are already exported from @aback-ui/ui.

  2. Apply the stored theme before paint

    Render ThemeScript in 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>
      );
    }
    suppressHydrationWarning is needed because the script changes the html class before React hydrates. Pass nonce when your Content Security Policy requires one.
  3. 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 toDarkLabel and toLightLabel.

Build your own control

useTheme reads and writes the same stored choice, for a menu or a settings page.

components/theme-select.tsxtsx
"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.