Blog
4 min read

How to Add Dark Mode to Your Website (CSS, Tailwind, and a Toggle)

Add dark mode the right way: follow the system setting with prefers-color-scheme, organise colours with CSS variables, use Tailwind's dark: variant, add a toggle that remembers the choice, and avoid the white flash on page load.

Dark mode used to be a nice extra. Now many people keep their whole device in dark mode, and a bright white page at night feels like a flashlight in the face. Adding it properly takes less code than you might think — if you set things up in the right order.

Step 1: Follow the system setting

Browsers tell your CSS whether the visitor's device is in dark mode with the prefers-color-scheme media query:

body {
  background: white;
  color: #111;
}

@media (prefers-color-scheme: dark) {
  body {
    background: #111;
    color: #eee;
  }
}

That alone gives you automatic dark mode with no JavaScript. Many sites need nothing more.

Also add this line, which tells the browser to darken its own built-in parts — scrollbars, form fields, checkboxes:

:root { color-scheme: light dark; }

Step 2: Use CSS variables for colours

Repeating colours in every rule doesn't scale. Define them once as variables, then swap the values for dark mode:

:root {
  --bg: #ffffff;
  --text: #111111;
  --muted: #666666;
  --accent: #2563eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1115;
    --text: #e8e8e8;
    --muted: #9a9a9a;
    --accent: #60a5fa;
  }
}

body { background: var(--bg); color: var(--text); }
a { color: var(--accent); }

Now every component uses var(--bg), and dark mode is one block of overrides. This is exactly how shadcn/ui themes work.

Step 3 (optional): a toggle that remembers

Some people want dark mode on a light device, or vice versa. A toggle needs three things: a class or attribute on <html>, CSS that responds to it, and a saved preference.

CSS keyed off an attribute:

:root[data-theme="dark"] {
  --bg: #0f1115;
  --text: #e8e8e8;
}

The toggle:

function setTheme(theme) {
  document.documentElement.dataset.theme = theme;
  localStorage.setItem("theme", theme);
}

Avoid the white flash

If the saved theme is applied by your app's JavaScript after it loads, dark-mode visitors see a flash of white first. Fix it with a tiny script at the very top of <head>, which runs before anything is drawn:

<script>
  const saved = localStorage.getItem("theme");
  const dark = saved ? saved === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
  document.documentElement.dataset.theme = dark ? "dark" : "light";
</script>

In Next.js, this early script is also why theme libraries ask you to add suppressHydrationWarning to <html> — see Next.js hydration errors.

Dark mode with Tailwind CSS

In Tailwind, prefix any class with dark::

<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">

By default, dark: follows the system setting. For a manual toggle in Tailwind v4, add this to your CSS so dark: responds to a class on <html> instead:

@custom-variant dark (&:where(.dark, .dark *));

Then toggle the dark class on <html> with the script above (using classList instead of dataset).

Designing a good dark theme

  • Don't use pure black. A very dark grey (#0f1115, #121212) is easier on the eyes and lets you show depth with slightly lighter surfaces.
  • Don't use pure white text on dark backgrounds — a light grey reduces glare.
  • Lighten your accent colours. A blue that works on white is often too dark on black.
  • Check contrast. Muted text that's fine on white can become unreadable on dark. Aim for the contrast ratios in web accessibility basics.
  • Check images and logos. A logo with dark text disappears on a dark background; provide a light version.
  • Shadows don't show on dark backgrounds — use borders or lighter surfaces instead.

The summary

  • prefers-color-scheme gives you automatic dark mode in pure CSS.
  • Put colours in CSS variables and override them for dark mode.
  • For a toggle: set an attribute or class on <html>, save it in localStorage, and apply it with an early inline script to avoid a flash.
  • In Tailwind, use dark:; for a toggle in v4, add a custom dark variant.

EasySpawn gives Claude Code a server where it can run your app and check both themes in a real browser, rather than guessing how dark mode will look. See how it works or join the waitlist.

Related: Responsive Design Basics · HTML, CSS and JavaScript Explained · What Is Tailwind CSS? · Add a Favicon to Your Website

Keep reading