Learn Tailwind CSS - Dark Mode, Theming & CSS Variables
Episode 6 of 23

Learn Tailwind CSS - Dark Mode, Theming & CSS Variables

This episode covers Tailwind's dark mode strategies: media versus class, along with the pros and cons of each. You also learn how to structure themes with theme.extend, use CSS custom properties for dynamic theme switching, plus integration with prefers-color-scheme and accessibility considerations.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Dark mode has become a feature users expect. Tailwind supports it through the dark: variant with two strategies: following the system preference via media, or being manually controlled through a class. Episode 6 covers both, how to structure themes with theme.extend, and the modern approach of using CSS variables for dynamic theming.

The choice of strategy heavily affects your architecture. media is the simplest and purely follows the system; class gives you full control, including manual toggles and persisting user preference. Most modern applications choose class for its flexibility.

Dark Mode: media vs class

The media strategy

This is Tailwind's default. The dark: variant activates automatically when the user's system is in dark mode:

HTMLAutomatic dark mode
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
  Konten menyesuaikan preferensi sistem.
</div>

Its advantage is zero configuration. Its drawback: you can't offer a manual toggle, because the decision is entirely in the operating system's hands.

The class strategy

Enable it with darkMode: "class" in the config:

JSEnabling the class strategy
module.exports = {
  darkMode: "class",
  content: ["./src/**/*.{html,js,ts,jsx}"],
  theme: { extend: {} },
  plugins: [],
};

Then add the dark class to a root element:

HTMLDark class on html
<html class="dark">
  <body class="bg-white dark:bg-gray-900">...</body>
</html>

darkMode: "class" makes dark: active only if there's an element with the dark class above it — usually html. Manual toggling is now possible by adding or removing that class with JavaScript.

Building a Theme with theme.extend

For large projects, avoid scattering color values directly in markup. Define tokens in theme.extend:

JSCustom color tokens
theme: {
  extend: {
    colors: {
      brand: {
        50: "#eff6ff",
        500: "#3b82f6",
        600: "#2563eb",
        900: "#1e3a8a",
      },
    },
  },
},

After that, bg-brand-500 and text-brand-600 become available as utilities. These tokens become the single source of truth for your design — changing one value here changes the whole application.

CSS Custom Properties and Dynamic Theme Switching

A more modern approach: define tokens as CSS variables in :root, then reference them from the config. This lets you swap themes at runtime without a rebuild:

JSTokens as CSS variables
:root {
  --color-bg: #ffffff;
  --color-text: #111827;
}
 
html.dark {
  --color-bg: #111827;
  --color-text: #f9fafb;
}
JSReferencing CSS variables
theme: {
  extend: {
    colors: {
      bg: "var(--color-bg)",
      text: "var(--color-text)",
    },
  },
},

Now bg-bg and text-text automatically follow the variable values, so switching themes is just a matter of swapping the values in CSS — Tailwind doesn't need rebuilding. This colors: { bg: "var(--color-bg)" } pattern is very useful for white-label or multi-tenant setups.

To persist the user's choice, save the preference and apply it before rendering:

JSDark mode toggle
const isDark = localStorage.getItem("theme") === "dark";
document.documentElement.classList.toggle("dark", isDark);

Move this script into the head so you don't get a flash of the wrong theme when the page loads.

prefers-color-scheme and Accessibility

If you want to still respect the system preference while using the class strategy, combine the two:

HTMLScript summarizing preferences
<script>
  if (localStorage.theme === "dark" ||
      (!("theme" in localStorage) &&
       window.matchMedia("(prefers-color-scheme: dark)").matches)) {
    document.documentElement.classList.add("dark");
  }
</script>

Accessibility considerations: make sure text-to-background contrast still meets the minimum ratio in both modes, and don't rely on color as the only status indicator — add an icon or text label.

Warning

Sufficient contrast in light mode isn't necessarily sufficient in dark mode. A token like gray-700 on white can fall below the 4.5:1 ratio once flipped. Always audit contrast in both themes before release.

Conclusion

Episode 6 equipped you with theming: the media and class strategies, structuring tokens via theme.extend, dynamic theme switching with CSS variables, and prefers-color-scheme integration that respects the system preference while still offering manual control.

Key takeaways:

  • The media strategy follows the system; the class strategy gives full control.
  • Enable class via darkMode: "class", then add the dark class on html.
  • Color tokens live in theme.extend.colors as the single source of truth.
  • CSS variables allow runtime theme switching without a rebuild.
  • Persist the user's preference and apply it in the head to prevent flashes.
  • Audit contrast and status indicators in both modes.

Next, in episode 7, we'll cover composing utilities — when to use @apply versus inline utilities, creating component classes inside the components layer, and advanced composition with variants and plugins for reusable patterns.