Tailwind Dark Mode & Theming
Users increasingly expect dark mode, and many products need several themes: a brand theme per customer, a high-contrast option, or seasonal variations. Tailwind supports this in two complementary ways. The dark: variant applies utilities conditionally (bg-white dark:bg-gray-950). Semantic color tokens backed by CSS variables (bg-surface) switch values globally by redefining the variables. Tailwind v4's CSS-first configuration makes the variable approach especially natural.
For small projects, dark: utilities are fine. For design systems and multi-theme apps, semantic tokens are more maintainable: components use bg-surface text-ink, and themes just redefine what those mean.
TL;DR
- By default,
dark:follows the OS setting viaprefers-color-scheme. - For a manual toggle, redefine the variant:
@custom-variant dark (&:where(.dark, .dark *));(or adata-themeattribute). - Prefer semantic tokens (
--color-surface,--color-ink) redefined per theme, over sprinklingdark:on every element. - Prevent the flash of the wrong theme with a tiny inline script in
<head>that sets the class before paint. - Support three states: light, dark, and system. Persist the choice, and set
color-schemefor native controls. - Check contrast in both themes, and don't just invert colors.
Quick Example
Core Concepts
The dark: Variant
Out of the box, dark: uses @media (prefers-color-scheme: dark), so it follows the operating system with zero JavaScript. To let users override that, redefine the variant with @custom-variant to depend on a class (.dark) or an attribute ([data-theme=dark]) on an ancestor, usually <html>. (In Tailwind v3 this was darkMode: 'class' in the JS config.)
Utilities vs Semantic Tokens
Most design systems use semantic tokens for everything themeable, and dark: for rare exceptions (inverting a logo, adjusting an illustration). See design tokens and CSS custom properties.
Avoiding the Theme Flash
If the theme is applied after JavaScript loads, or after React hydrates, users see a flash of light content before dark mode kicks in. Fixes:
- An inline blocking script in
<head>that reads the stored preference, or the system preference, and sets the class or attribute before first paint. - With SSR, store the preference in a cookie so the server renders the correct attribute directly.
- Libraries like
next-themesimplement both patterns for React and Next.js.
Light, Dark, and System
Offer three options: Light, Dark, and System (follow the OS). With "System", listen for changes with matchMedia('(prefers-color-scheme: dark)').addEventListener('change', …), so the page updates if the OS switches at sunset. Persist explicit choices in localStorage or a cookie, or in the user's profile for signed-in users.
color-scheme
Setting color-scheme: light or dark on the root tells the browser to render native UI (scrollbars, form controls, date pickers, default backgrounds) in the matching scheme. Without it, dark pages can show bright white scrollbars and inputs.
Multiple Themes
Because themes are just variable sets, adding brand or tenant themes is straightforward:
Combine brand and mode (data-brand="acme" plus data-theme="dark") with layered variable definitions. For white-label SaaS, load tenant token values from configuration and inject them as CSS variables at runtime. See dark mode theming.
Best Practices
Design Dark Themes Deliberately
Dark mode isn't inversion. Use dark grays rather than pure black for surfaces, raise elevation with lighter surfaces instead of shadows, desaturate and lighten accent colors, and reduce the intensity of large bright areas.
Check Contrast in Every Theme
Verify WCAG contrast ratios (4.5:1 for body text, 3:1 for large text and UI components) for both light and dark token sets, including muted text, borders, and focus rings. See accessibility.
Theme Images, Charts, and Code Blocks
Use <picture> with prefers-color-scheme sources or dark: variants for images, theme-aware chart palettes, and syntax highlighting themes that switch with the page. These are often forgotten.
Test Both Themes Continuously
Include dark mode in visual regression tests and Storybook, since components added without tokens often look broken in the theme nobody checked.
Common Mistakes
Toggling the Class After Hydration
Setting class="dark" in a React useEffect guarantees a flash on every load. Use a head script or server-rendered attribute.
Hard-Coded Colors in Components
bg-white text-black inside a component ignores themes entirely. Use semantic tokens so components adapt.
Pure Black Backgrounds With Pure White Text
Maximum contrast causes halation (text appears to glow and blur) and eye strain for many readers. Use off-black surfaces and slightly off-white text.
FAQ
How do I enable class-based dark mode in Tailwind v4?
Override the dark variant in your CSS: @custom-variant dark (&:where(.dark, .dark ));, then toggle the dark class on <html>. For an attribute, use &:where([data-theme=dark], [data-theme=dark] ). Without the override, dark: follows the OS preference.
How do I prevent dark mode flicker on page load?
Apply the theme before the page renders: an inline script in <head> that reads the saved preference (or the system preference) and sets the class or attribute synchronously, or server-render the attribute from a cookie. Setting it after JavaScript frameworks load causes a visible flash.
Should I use dark: utilities or CSS variables?
For small sites, dark: utilities are quick and explicit. For design systems, multi-theme apps, or large codebases, define semantic tokens as CSS variables and switch their values per theme. Components stay clean and themes stay consistent.
How do I support more than two themes?
Define each theme as a set of CSS variable values under its own selector (for example [data-theme="ocean"]), with components using only semantic tokens. Switching themes is then just changing an attribute. No extra utility classes are needed.
Related Topics
- Tailwind — The framework overview
- Tailwind Theme Configuration — Defining tokens in @theme
- Tailwind Variants — The dark: variant and custom variants
- Dark Mode Theming — Design principles for dark themes
- CSS Custom Properties — Runtime theme variables
- Accessibility — Contrast requirements