Dark Mode

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

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 .dark class on the <html> element
  • Temporarily disabling transitions and animations to avoid flicker when toggling
  • Storing the user’s preference in localStorage and 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>

How to use

Hummingbird supports dark mode out of the box, and all components are fully compatible with it. No additional configuration is required to enable dark mode styles.

To apply additional styling specifically for dark mode, use the dark: variant alongside utility classes. This ensures that elements automatically adjust their appearance whenever the .dark class is active.

<div class="bg-subtle text-primary dark:text-secondary">
  This content adapts to dark mode
</div>

In addition to using the dark: variant, Hummingbird’s theme variables can be directly overridden within a dark scope:

:root, :host { 
  @variant dark {
    --color-primary: var(--color-blue-400);
    --color-secondary: var(--color-purple-400);
  }
}

See all available variables in the Theming section.