Dark Mode
How Dark Mode Works in Tailwind
Tailwind supports dark mode through the dark: variant. The idea is simple: you define base (light) styles, then override or extend them when dark mode is active.
Two strategies: media vs class
Tailwind can enable dark mode using system preference (media) or by toggling a class (class). In production, class is usually better because you can provide a user toggle and store the preference.
Config example
module.exports = {
darkMode: "class",
content: ["./public/**/*.php", "./assets/**/*.js"],
theme: { extend: {} },
plugins: [],
};
How class-based dark mode activates
When you use class mode, dark mode becomes active when a parent element (often html) has the class dark.
<html class="dark"> ... </html>
Basic dark mode pattern
<div class="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"> ... </div>
Think in roles, not random colors
The easiest way to keep dark mode consistent is to define roles: page background, surface, border, text, muted text, and primary. Then apply those roles everywhere.
Recommended role mapping
- Light page background: slate-50
- Dark page background: slate-950 or slate-900
- Light surface: white
- Dark surface: slate-900 or slate-800
- Light border: slate-200
- Dark border: slate-700
- Light text: slate-900
- Dark text: slate-100
- Light muted text: slate-600
- Dark muted text: slate-400
Production surface example
<div class="bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 min-h-screen">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg p-6">
Card surface
</div>
</div>
Borders and dividers in dark mode
Dark borders should be subtle but visible. Too dark borders look harsh; too light borders disappear. A common safe choice is slate-700 on dark surfaces.
Interactive states (hover/focus)
Hover and focus states must also work in dark mode. Do not rely on “slightly darker” backgrounds because dark surfaces have less visible contrast.
<a class="block rounded-lg p-4
bg-white hover:bg-slate-50
dark:bg-slate-900 dark:hover:bg-slate-800
border border-slate-200 dark:border-slate-700">
Hoverable row
</a>
Focus rings in dark mode
Rings remain important for accessibility. Use the same ring color but ensure enough contrast against dark surfaces. ring-offset is useful to separate the ring from the element background.
<button class="px-4 py-2 rounded-md bg-blue-600 text-white
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
focus:ring-offset-white dark:focus:ring-offset-slate-900">
Save
</button>
Avoid “flash of wrong theme”
In production, class-based dark mode can show a brief flash of light theme before your JavaScript adds the dark class. To avoid this, set the class as early as possible (inline script in the head) and persist preference (localStorage or cookie).
Common mistakes
- Only switching backgrounds but not borders and muted text.
- Using different neutral families (gray vs slate) inconsistently across dark mode.
- Hover states that are invisible in dark mode.
- Removing focus styles, making keyboard navigation impossible.
- Relying on random hex colors instead of role-based tokens.
Production checklist
- Use darkMode: "class" when you need a user toggle.
- Define consistent roles (bg, surface, border, text, muted).
- Ensure hover and focus states work in both themes.
- Prevent theme flash by applying the class early.
- Keep neutrals consistent (pick slate or gray and stick to it).