esistdj
Back to TILs
TIL

Fixing Tailwind Dark Mode with Custom Variants

2 min read
tailwindcssdark-modetheming

TIL: Fixing Tailwind Dark Mode with Custom Variants

Today I learned how to fix a frustrating issue where Tailwind's dark mode colors weren't working correctly when my manual theme toggle conflicted with the system's prefers-color-scheme setting.

The Problem

I had implemented a manual dark mode toggle using a .dark class on the <html> element. However, even when I selected "Light Mode," the text was showing white/light colors because my system was set to dark mode. The CSS media query @media (prefers-color-scheme: dark) was overriding my manual theme selection!

In other words: If your system preference was dark, the site would apply dark mode styles even when you explicitly chose light mode.

The Root Cause

By default, Tailwind v4 applies dark mode utilities based on prefers-color-scheme: dark, which checks the user's operating system settings. This means even with darkMode: 'class' in your config, the system preference could still interfere.

The Solution

Override the dark mode variant in your CSS to use only the .dark class selector, ignoring prefers-color-scheme entirely:

@import "tailwindcss";

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

This single line tells Tailwind:

  • Only apply dark:* utilities when the .dark class is present
  • Ignore the system's prefers-color-scheme setting
  • Give you full control over theme switching via JavaScript

How It Works

The @custom-variant directive creates a custom variant that:

  1. Checks for .dark class on the element itself (&:where(.dark))
  2. Or checks for .dark class on any ancestor (&:where(.dark *))
  3. Applies dark mode styles only when this condition is met

Three-State Theme System

With this fix, you can now implement a proper three-state theme system:

// light: Force light mode (ignore system preference)
// dark: Force dark mode (ignore system preference)  
// system: Respect prefers-color-scheme

if (theme === "system") {
  const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
    .matches ? "dark" : "light";
  document.documentElement.classList.add(systemTheme);
} else {
  document.documentElement.classList.add(theme);
}

References

Key Takeaway

When implementing manual dark mode with Tailwind, always override the dark variant to prevent prefers-color-scheme from interfering with your theme toggle. This gives you complete control over your theme system! 🎨