Dark mode
The dark: variant applies a utility when the dark theme is active: bg-white dark:bg-gray-900 is a white background by day and a dark one at night. What remains is to decide who turns the dark theme on — the operating system or a button on the site.
Following the system
By default dark: follows the user's system setting — the prefers-color-scheme: dark media query. Nothing needs configuring:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<div class="rounded-xl bg-white p-4 text-gray-900 dark:bg-gray-900 dark:text-white">
Theme follows the system
</div>If your system uses a dark theme, the block is dark; otherwise it is light. Each color gets a pair: the light one without a prefix and the dark one with dark:.
A toggle on the site
To let the user pick a theme with a button, dark: is redefined with the @custom-variant directive — now it applies when the root element has the dark class. In a project this line goes into the main CSS file, for example src/style.css, after @import "tailwindcss";:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));The example below does the same for the browser script, and the button adds and removes the dark class on the <html> tag:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
</style>
<div class="max-w-xs rounded-xl bg-white p-4 shadow dark:bg-gray-800">
<h2 class="font-semibold text-gray-900 dark:text-white">Wireless headphones</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">Noise cancelling and 30 hours of playback.</p>
<button id="theme" class="mt-3 rounded-lg bg-gray-900 px-3 py-1 text-sm text-white dark:bg-white dark:text-gray-900">
Switch theme
</button>
</div>
<script>
document.getElementById('theme').addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
});
</script>Press the button — the card turns dark; press it again — light. The choice is usually saved in localStorage and restored on load.
With a data attribute
An attribute can be used instead of a class — handy when there are more than two themes:
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));Then <html data-theme="dark"> turns the dark theme on.
Which way to choose
| Way | When it fits |
|---|---|
| Default (system) | No toggle needed |
The dark class | There is a theme button |
data-theme | Several themes, or the attribute is already in use |
Common mistake
Adding @custom-variant and expecting the site to keep following the system theme. After the redefinition dark: reacts only to the class: if no script put dark on <html>, the page is always light. On load the script must pick the theme itself — from localStorage, or, if that is empty, from matchMedia('(prefers-color-scheme: dark)').