Dark Mode

This guide explains how to enable and manage dark mode in a React application, in a way consistent with Tailwind's dark-mode documentation.

How it works

Hummingbird uses shared CSS theme variables to power both light and dark modes.

  • The default (light) colors are defined in @theme.
  • The dark palette overrides those same variables inside @variant dark.
  • Toggling a .dark class switches all values globally.

Key idea

All components rely on the same semantic tokens:

  • --background-color-default
  • --text-color-default
  • --color-primary
  • --border-color-default

Result

Switching themes requires no component changes — only variable values update. Every Hummingbird React component adapts automatically.

Customizing dark mode

Override variables after importing Hummingbird:

@import "tailwindcss";
@import "@hummingbirdui/react";

@theme {
  --background-color-default: var(--color-white);
  --text-color-default: var(--color-gray-800);
  --color-primary: var(--color-blue-500);
}

@layer theme {
  :root,
  :host {
    @variant dark {
      --background-color-default: var(--color-gray-950);
      --text-color-default: var(--color-gray-100);
      --color-primary: var(--color-blue-400);
    }
  }
}

Setting up dark mode

1. Custom dark mode selector

Enable class-based dark mode after importing Tailwind and Hummingbird styles:

@custom-variant dark (&:where(.dark, .dark *), .dark);

2. Set the initial theme

Render ThemeModeScript inside <head> to apply the saved theme before first paint - no flash of the wrong theme. Safe in React Server Components.

// app/layout.tsx
import { ThemeModeScript } from "@hummingbirdui/react";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeModeScript />
      </head>
      <body>{children}</body>
    </html>
  );
}
PropTypeDefault
defaultMode"light" | "dark" | "system""system"

3. Add a theme toggle

DarkThemeToggle switches between light and dark mode, persists the choice, and keeps all tabs in sync.

"use client";

import { DarkThemeToggle } from "@hummingbirdui/react";

export function Navbar() {
  return <DarkThemeToggle />;
}

Pass your own icons to replace the default sun/moon:

import { Lightbulb, LightbulbOff } from "lucide-react";

<DarkThemeToggle iconDark={LightbulbOff} iconLight={Lightbulb} />;

It is built on Button, so all Button props work too.

PropTypeDefault
iconDarkReact.FC<React.ComponentProps<'svg'>>SunIcon
iconLightReact.FC<React.ComponentProps<'svg'>>MoonIcon

The useThemeMode hook

For custom controls, use useThemeMode. It applies the .dark class, follows the OS preference in "system" mode, and stays in sync across components and browser tabs. DarkThemeToggle uses it internally.

"use client";

import { useThemeMode, type ThemeMode } from "@hummingbirdui/react/hooks";

export function ThemeSelect() {
  const { mode, setMode } = useThemeMode();

  return (
    <select
      value={mode}
      onChange={(event) => setMode(event.target.value as ThemeMode)}
    >
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
  );
}
PropTypeDefault
mode"light" | "dark" | "system"
computedMode"light" | "dark"
setMode(mode: ThemeMode) => void
toggleMode() => void
clearMode() => void

Dark mode variable overrides

A list of theme variables that are overridden when dark mode is active.

@layer theme {
  :root,
  :host {
    @variant dark {
      /* Background colors */
      --background-color-subtle: var(--color-gray-900);
      --background-color-muted: var(--color-gray-800);
      --background-color-default: var(--color-gray-950);
      --background-color-highlight: var(--color-gray-700);
      --background-color-emphasis: var(--color-gray-600);

      /* Text colors */
      --text-color-subtle: var(--color-gray-500);
      --text-color-muted: var(--color-gray-400);
      --text-color-default: var(--color-gray-100);
      --text-color-highlight: var(--color-gray-50);
      --text-color-emphasis: var(--color-white);

      /* Neutral */
      --color-neutral: var(--color-gray-700);
      --color-inverse: var(--color-gray-950);

      /* Primary */
      --color-primary-lighter: var(--color-blue-950);
      --color-primary-light: var(--color-blue-700);
      --color-primary: var(--color-blue-400);
      --color-primary-dark: var(--color-blue-300);
      --color-primary-darker: var(--color-blue-100);

      /* Secondary */
      --color-secondary-lighter: var(--color-purple-950);
      --color-secondary-light: var(--color-purple-700);
      --color-secondary: var(--color-purple-400);
      --color-secondary-dark: var(--color-purple-300);
      --color-secondary-darker: var(--color-purple-100);

      /* Danger */
      --color-danger-lighter: var(--color-red-950);
      --color-danger-light: var(--color-red-600);
      --color-danger: var(--color-red-400);
      --color-danger-dark: var(--color-red-300);
      --color-danger-darker: var(--color-red-200);

      /* Warning */
      --color-warning-lighter: var(--color-orange-950);
      --color-warning-light: var(--color-orange-800);
      --color-warning: var(--color-orange-400);
      --color-warning-dark: var(--color-orange-300);
      --color-warning-darker: var(--color-orange-200);

      /* Success */
      --color-success-lighter: var(--color-green-950);
      --color-success-light: var(--color-green-700);
      --color-success: var(--color-green-400);
      --color-success-dark: var(--color-green-300);
      --color-success-darker: var(--color-green-200);

      /* Info */
      --color-info-lighter: var(--color-sky-950);
      --color-info-light: var(--color-sky-700);
      --color-info: var(--color-sky-400);
      --color-info-dark: var(--color-sky-300);
      --color-info-darker: var(--color-sky-200);

      /* Borders */
      --border-color-subtle: var(--color-gray-800);
      --border-color-default: var(--color-gray-700);
      --border-color-emphasis: var(--color-gray-600);

      /* Actions */
      --color-active: var(--color-gray-500);
      --color-hover: var(--color-gray-700);
      --color-selected: var(--color-gray-900);
      --color-disabled-color: var(--color-gray-500);
      --color-disabled: var(--color-gray-700);
      --color-focus: var(--color-gray-700);

      /* Shadows */
      --shadow-xl:
        0px 12px 51px 0px rgba(0, 0, 0, 0.6),
        0px 3px 24px 0px rgba(0, 0, 0, 0.56),
        0px 1px 16px 0px rgba(0, 0, 0, 0.1);
    }
  }
}