Dark Mode

intermediate
12 min

Dark Mode

Dark mode has gone from novelty to expectation: users choose it at the operating-system level and expect sites to respect it. Tailwind CSS makes dark theming almost mechanical with the dark: variant. In this lesson you will learn both dark mode strategies, how to design a dark palette that does not look muddy, and how to build a working theme toggle.

The dark: Variant

Any utility prefixed with dark: applies only in dark mode:

html
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <h1 class="text-2xl font-bold">Adaptive card</h1> <p class="text-gray-600 dark:text-gray-400">Same markup, two themes.</p> </div>

You write the light theme normally, then add dark overrides beside each color decision.

Strategy 1: Follow the System (media)

By default, dark: responds to the prefers-color-scheme media query. If the visitor has chosen dark at the OS level, dark styles apply automatically. Zero JavaScript, zero configuration — but the user cannot override it per-site.

Strategy 2: Manual Toggle (class)

Most production sites want a toggle button. Configure Tailwind to key dark mode off a class or attribute on the root element instead. In modern Tailwind you register a custom variant in CSS:

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

(In Tailwind v3 the equivalent was darkMode: "class" in tailwind.config.js.) Now dark styles apply whenever <html class="dark"> is present, and you control that class with a few lines of JavaScript:

js
const btn = document.querySelector("#theme-toggle"); btn.addEventListener("click", () => { document.documentElement.classList.toggle("dark"); localStorage.theme = document.documentElement.classList.contains("dark") ? "dark" : "light"; });

Persisting the choice to localStorage and applying it in a tiny inline script in <head> prevents the dreaded flash of the wrong theme on page load.

Designing a Good Dark Palette

Straight color inversion looks terrible. A few battle-tested guidelines:

  • Do not use pure black; gray-900 or slate-900 backgrounds are easier on the eyes.
  • Do not use pure white text; gray-100 or gray-200 reduces glare.
  • Reduce shadow reliance — shadows disappear on dark backgrounds. Use borders (dark:border-gray-700) or lighter surfaces to show elevation.
  • Desaturate slightly: a blue-500 button may read better as blue-400 on dark surfaces.

A typical component conversion:

html
<article class="rounded-xl bg-white shadow-md p-6 dark:bg-gray-800 dark:shadow-none dark:border dark:border-gray-700"> <h2 class="text-gray-900 dark:text-white">Title</h2> <p class="text-gray-600 dark:text-gray-300">Body copy.</p> <button class="bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-400">Action</button> </article>

Combining dark: with Other Variants

Variants stack in a predictable order: dark:hover:bg-gray-700 styles hover in dark mode, and dark:md:bg-black applies dark styles only at medium screens and up. This composability is what makes a complete dual theme achievable with utilities alone.

Auditing Your Dark Theme

Toggle dark mode and walk every page looking for four common bugs: black-on-black text, images with white backgrounds (wrap them or add dark:bg-white rounded), invisible borders, and hard-coded colors in third-party embeds. Fixing these is usually a one-utility patch each.

Key Takeaways

  • The dark: variant applies utilities only in dark mode.
  • Media strategy follows the OS automatically; class strategy enables a user toggle.
  • Persist the toggle in localStorage and apply it early to avoid theme flash.
  • Use dark grays instead of black, off-whites instead of white, and borders instead of shadows.
  • dark: stacks with hover:, breakpoints, and every other variant.

Next up: Customizing the Theme and Config, where you will teach Tailwind your brand colors and fonts.

Dark Mode - Tailwind CSS | CodeYourCraft | CodeYourCraft