Skip to content

Tailwind CSS v4 Dark Mode — Configuration and Custom Strategies

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Tailwind CSS v4 Dark Mode. We cover key concepts, practical examples, and best practices to help you master this topic.

Tailwind CSS v4 dark mode is configured via @variant dark directive, replacing the v3 darkMode config option, with flexible strategies for system preference, class toggling, and custom selectors.

What You'll Learn

You will learn how to configure dark mode using @variant in v4, implement system preference and class-based strategies, use dark: variant prefixes, and create custom dark mode variants.

Why It Matters

Dark mode is an accessibility necessity. DodaTech's v4 migration simplified dark mode setup from 5 lines of JavaScript config to 1 line of CSS @variant.

Real-World Use

Durga Antivirus Pro v4 uses @variant dark (&:where(.dark, .dark *)) with localStorage persistence for user-toggleable dark mode.

flowchart LR
    A[Custom Variants] --> B[Dark Mode v4]
    B --> C[@variant dark]
    B --> D[System Strategy]
    B --> E[Class Strategy]
    B --> F[Custom Strategy]
    style B fill:#38bdf8,stroke:#0284c7,color:#fff
    style C fill:#22c55e,stroke:#16a34a,color:#fff

System Preference Strategy

@import "tailwindcss";

/* System preference dark mode (follows OS setting) */
@variant dark (@media (prefers-color-scheme: dark));
<div class="bg-white dark:bg-gray-900 p-6 rounded-lg transition-colors">
  <h2 class="text-gray-900 dark:text-white font-bold">System Dark Mode</h2>
  <p class="text-gray-600 dark:text-gray-300 mt-2">
    Automatically follows OS dark mode setting.
  </p>
</div>

Expected output: Dark styles apply when the user's system dark mode is enabled. No JavaScript required.

Class Toggle Strategy

@import "tailwindcss";

/* Class-based dark mode (manual toggle) */
@variant dark (&:where(.dark, .dark *));
<script>
  // Toggle function
  function toggleDark() {
    document.documentElement.classList.toggle('dark');
    localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
  }

  // On page load
  if (localStorage.getItem('darkMode') === 'true') {
    document.documentElement.classList.add('dark');
  }
</script>

<button onclick="toggleDark()" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white rounded-lg">
  Toggle Dark Mode
</button>

Expected output: Toggle button adds/removes the dark class on the html element. Dark: variants respond to the class. Preference persists via localStorage.

Combined Strategy (Respect OS + Allow Override)

@import "tailwindcss";

/* Check both OS preference and manual class */
@variant dark (&:where(.dark, .dark *));

/* Also respect OS preference if no manual toggle set */
@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    --dark-mode: true;
  }
}
<script>
  // On load: check localStorage first, then OS preference
  const stored = localStorage.getItem('darkMode');
  if (stored === 'true' || (stored === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark');
  }

  // Toggle with localStorage
  function toggleDark() {
    document.documentElement.classList.toggle('dark');
    localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
  }

  // Listen for OS changes
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    if (localStorage.getItem('darkMode') === null) {
      document.documentElement.classList.toggle('dark', e.matches);
    }
  });
</script>

Expected output: Dark mode follows OS preference by default but allows manual override. OS changes apply if no manual toggle has been set.

Custom Dark Mode Selectors

@import "tailwindcss";

/* Multiple selector support */
@variant dark (&:where(
  [data-theme="dark"],
  .dark,
  .theme-dark
));

/* Component-scoped dark mode */
@variant dark-card (.dark-card &);

/* Per-section dark mode */
@variant dark-section (.dark-section &);
<!-- Data attribute approach -->
<div data-theme="dark">
  <div class="bg-white dark:bg-gray-800 p-4 rounded">
    Dark mode via data-theme attribute
  </div>
</div>

<!-- Per-component dark mode -->
<div class="dark-card">
  <div class="bg-white dark-card:bg-gray-800 p-4 rounded">
    This card is always dark.
  </div>
</div>

Expected output: Dark mode can be scoped to specific containers or triggered by custom attributes, not just the html element.

Dark Mode Transition

@import "tailwindcss";

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

/* Smooth theme transition */
*, *::before, *::after {
  transition: background-color 0.3s ease,
              border-color 0.3s ease,
              color 0.3s ease;
}

/* Respect reduced motion */
@variant reduced-motion (@media (prefers-reduced-motion: reduce));

.reduced-motion *,
.reduced-motion *::before,
.reduced-motion *::after {
  transition: none !important;
}

Expected output: All themed properties transition smoothly. Users who prefer reduced motion see instant changes without animation.

Common Mistakes

1. Not Defining @variant dark

Without an explicit @variant dark definition, the dark: prefix does not work. v4 does not have a default dark mode strategy.

2. Using v3 darkMode Config

The darkMode: 'class' in tailwind.config.js does not work in v4. Use @variant dark (&:where(.dark, .dark *)) in CSS.

3. Missing Dark Mode Colors for All Elements

Backgrounds, text, borders, and rings all need dark: variants. Use @layer base for global dark styles.

4. Not Handling localStorage on Page Load

Without checking localStorage on page load, the dark mode preference resets every time the page is refreshed.

5. Ignoring system Preference

Users expect dark mode to follow their OS setting. Always check prefers-color-scheme on initial load.

Practice Questions

  1. How is dark mode configured in v4? Using @variant dark in CSS. Example: @variant dark (@media (prefers-color-scheme: dark)) for system mode.

  2. How do you use class-based dark mode in v4? @variant dark (&:where(.dark, .dark *)) and toggle the dark class on html element.

  3. What is the advantage of CSS-first dark mode? No JavaScript config file needed. Dark mode is defined entirely in CSS.

  4. How do you persist dark mode preference? Save to localStorage and apply on page load before the first render.

  5. Can dark mode be scoped to specific components? Yes. Define scoped variants: @variant dark-card (.dark-card &).

Challenge

Implement a complete dark mode system: combined OS + class strategy, localStorage persistence, OS change listener, motion-respecting transitions, and a toggle button with sun/moon icon.

FAQ

Do I need JavaScript for dark mode in v4?

For system preference only, no JavaScript is needed. For toggle buttons and persistence, JavaScript is required.

Can I use both system and manual dark mode?

Yes. Use the combined strategy: check localStorage, fall back to OS preference, and listen for OS changes.

How do I prevent flash of wrong theme?

Apply the dark class in a blocking script in the before any rendering.

Does dark mode work with container queries?

Yes. Combine dark: with container query variants: dark:@container-sm:bg-gray-800

Can I have multiple dark themes?

Yes. Use separate @variant definitions: @variant dark-blue (.dark-blue &), @variant dark-green (.dark-green &)

Mini Project

Build a complete dark mode system: SSR-safe initialization (blocking script in head), combined OS/class strategy with localStorage, smooth CSS transitions with motion-reduce, toggle button with icon swap, and scoped dark mode for a settings panel.

What's Next

Now master Container Queries for container-based Responsive Design. Combine dark mode with container queries for fully adaptive components.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro