Typography and color
Add a dark color scheme
Use dark variants to define deliberate alternate colors instead of mechanically inverting the interface.
Dark mode in Tailwind is a variant. You write the light styles as the base, and add dark: versions of the properties that change:
<div class="bg-white text-gray-950 dark:bg-gray-950 dark:text-white">...</div>
By default, dark: follows the operating system through the prefers-color-scheme media feature. Switch your system to dark and the box flips. No JavaScript involved.
Only override what changes. Padding, layout, and typography stay the same in both schemes, so they don’t need a dark: twin.
A manual switch
Most sites want a toggle, not just the system setting. In Tailwind v4 you redefine the dark variant to look at an attribute instead of the media query:
@import "tailwindcss";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
Now any element inside an ancestor with data-theme="dark" uses the dark styles:
<html data-theme="dark">...</html>
Your toggle just sets or removes that attribute on <html>.
Three states, not two
A good switch offers light, dark, and system. Store the explicit choice in localStorage. When the user picks “system”, drop the attribute and watch prefers-color-scheme with matchMedia. And set the attribute in a small inline script in <head>, before the page paints. Otherwise the page flashes light and then snaps to dark on every load.
Dark mode is a design, not an inversion
Go through every color with a role: page background, elevated surfaces, normal and muted text, borders, links, buttons, focus rings, success, warning, error, code blocks, shadows, and images. Pure white text on pure black glares. Gray-on-gray that looked fine in light mode often fails contrast in dark.
Set color-scheme too, so native controls, form widgets, and scrollbars match. Tailwind has scheme-light and scheme-dark utilities for this. Apply the one that matches the active theme.
Tokens reduce the pain
If every component repeats bg-white dark:bg-gray-950, you have a pattern. Define semantic tokens like --color-surface and change their values under the dark condition. Components then use bg-surface and get both schemes for free.
Try this: implement light, dark, and system for one card. Reload before and after changing the system theme and watch for a flash. Then go through every interactive state in both schemes.
Lesson completed