3 Topics
Chapter•50 Minutes

Dark Mode and Theme Design

Implement a coherent dark theme, understand class-driven theme switching, and keep contrast and design tokens consistent.

Dark variant basics

The dark: variant applies a style when the configured dark-mode condition is active. Depending on the Tailwind version and project setup, dark mode can follow the system preference or a class/selector controlled by your application. Confirm the active strategy in your version's documentation.

Do not simply invert every color. Dark themes need a deliberate surface hierarchy: page background, card surface, raised surface, primary text, muted text, borders, and status colors. Shadows may be less noticeable on dark backgrounds, so borders and subtle surface changes often do more work.

A card with light and dark themes
<article class="rounded-2xl border border-slate-200 bg-white p-6 text-slate-900 shadow-sm dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
  <h2 class="text-lg font-semibold">Workspace summary</h2>
  <p class="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-300">
    Your team has completed 42 tasks this week.
  </p>
</article>
đź’ˇ Light styles are the default. dark:* utilities override the relevant properties when the dark condition is active. Text and border colors change as well as the surface, preserving a hierarchy instead of changing only the background.
Knowledge Check

What is a good dark-mode strategy?

System preference and a manual theme toggle

A system-preference theme respects the user's operating-system setting. A manual toggle lets the user override that preference. For a manual theme, your app typically stores a theme choice, updates the selector or class that activates dark mode, and prevents a flash of the wrong theme during initial rendering.

In server-rendered frameworks, theme initialization can cause hydration differences if the server and client render different markup. Use a framework-appropriate theme strategy and test the first paint, refresh, and navigation. Never assume a CSS variant alone creates a functioning toggle.

Separate appearance from behavior

Tailwind styles the theme states. Your app still needs to decide where the theme value lives, how it persists, and how the active dark selector is applied.

Conceptual theme toggle
function ThemeToggle({ theme, setTheme }) {
  const isDark = theme === "dark";

  return (
    <button
      type="button"
      aria-pressed={isDark}
      onClick={() => setTheme(isDark ? "light" : "dark")}
      className="rounded-lg border px-3 py-2 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2"
    >
      Switch to {isDark ? "light" : "dark"} mode
    </button>
  );
}
đź’ˇ This component illustrates the accessible control contract; it assumes the parent owns and persists theme, and that the application's dark selector is wired to that state. aria-pressed communicates the toggle state. The code alone does not add or remove a class on the document.

Theme tokens and contrast

A maintainable theme uses semantic tokens rather than scattering arbitrary hex values across components. Think in roles—surface, surface-raised, text-primary, text-muted, border-subtle, brand, and danger—then map each role to light and dark values.

Check text contrast, disabled controls, form errors, charts, and focus rings in both themes. A muted color that looks elegant on a white background may become unreadable on a dark surface. Test real states, not just the default card.

Specifications & Requirements
  • Theme layer:Semantic design tokens
  • Validation:Contrast, focus, disabled, error, hover
  • Persistence:Application responsibility
  • Avoid:Uncoordinated one-off colors
Key Takeaways
  • Use a coordinated palette rather than simply inverting colors.
  • A theme toggle needs application state and persistence.
  • Test accessibility states in both light and dark themes.