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
.darkclass 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>
);
}| Prop | Type | Default |
|---|---|---|
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.
| Prop | Type | Default |
|---|---|---|
iconDark | React.FC<React.ComponentProps<'svg'>> | SunIcon |
iconLight | React.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>
);
}| Prop | Type | Default |
|---|---|---|
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);
}
}
}