Dark Mode
This guide explains how to enable and manage dark mode 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 markup changes—only variable values update.
Customizing dark mode
Override variables after importing Hummingbird:
@import "@hummingbirdui/hummingbird";
@theme {
--background-color-default: var(--color-white);
--text-color-default: var(--color-gray-800);
--color-primary: var(--color-blue-500);
}
:root, :host {
@variant dark {
--background-color-default: var(--color-gray-950);
--text-color-default: var(--color-gray-100);
--color-primary: var(--color-blue-400);
}
}Toggling dark mode manually
1. Custom dark mode selector
Hummingbird supports class-based dark mode using predefined color variables. To enable dark mode with a custom selector (instead of prefers-color-scheme), add the following after importing Tailwind and Hummingbird styles:
@custom-variant dark (&:where(.dark, .dark *), .dark);The .dark class is typically added or removed on the <html> element.
2. Set the initial theme
Add this inline script inside the <head> tag to ensure the correct theme is applied before the page renders. This prevents flash issues and respects both the user’s saved preference in localStorage and their system’s prefers-color-scheme setting.
<script>
// On page load or when changing themes, best to add inline in head to avoid FOUC
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
</script>3. Theme toggle script
This script manages switching between light and dark themes. It works by:
- Adding or removing the
.darkclass on the<html>element - Temporarily disabling transitions and animations to avoid flicker when toggling
- Storing the user’s preference in
localStorageand restoring it automatically on page load
const THEME_KEY = 'theme';
const THEMES = {
LIGHT: 'light',
DARK: 'dark',
SYSTEM: 'system',
};
// helpers for managing theme state
const getSystemTheme = () => (window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.DARK : THEMES.LIGHT);
const getStoredTheme = () => localStorage.getItem(THEME_KEY) ?? THEMES.SYSTEM;
const resolveTheme = (theme) => (theme === THEMES.SYSTEM ? getSystemTheme() : theme);
const applyTheme = (theme) => {
const html = document.documentElement;
const resolvedTheme = resolveTheme(theme);
html.classList.add('disable-transition');
html.classList.toggle('dark', resolvedTheme === THEMES.DARK);
requestAnimationFrame(() => html.classList.remove('disable-transition'));
};
const toggleTheme = () => {
const currentTheme = resolveTheme(getStoredTheme());
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
localStorage.setItem(THEME_KEY, newTheme);
applyTheme(newTheme);
};
document.addEventListener('DOMContentLoaded', () => {
applyTheme(getStoredTheme());
// sync theme when os preference changes (only in "system" mode)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
if (getStoredTheme() !== THEMES.SYSTEM) return;
applyTheme(THEMES.SYSTEM);
});
// theme toggle button
document.addEventListener('click', (event) => {
const toggleBtn = event.target.closest('[data-theme-toggle-btn]');
if (!toggleBtn) return;
toggleTheme();
});
});4. Toggle button markup (example)
Any preferred markup can be used for toggling icon visibility based on the current state.
<button type="button" data-theme-toggle-btn class="btn btn-circle btn-subtle-neutral">
<svg class="dark:hidden"><!-- moon icon --></svg>
<svg class="hidden dark:block"><!-- sun icon --></svg>
</button>
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-light: var(--color-gray-800);
--color-dark: var(--color-gray-700);
--color-contrast: 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);
}
}
}