← Back to articles

How to Add Dark Mode to Any Website (2026 Guide)

Dark mode is no longer optional. 82% of users prefer dark mode on at least some apps, and it's an accessibility feature for users with light sensitivity. Here's how to implement it properly.

The Quick Version

If you're using Tailwind CSS + Next.js, the fastest path:

npm install next-themes
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  )
}
// tailwind.config.ts
export default {
  darkMode: 'class',
  // ...
}
/* Use Tailwind dark: prefix */
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">

Done. But if you want to understand what's happening or aren't using Tailwind, read on.

Approach 1: CSS Media Query (System Preference)

The simplest approach — respect the user's OS dark mode setting automatically.

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --surface: #f5f5f5;
  --border: #e0e0e0;
  --primary: #2563eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0a0a0a;
    --text: #e5e5e5;
    --surface: #1a1a1a;
    --border: #2a2a2a;
    --primary: #60a5fa;
  }
}

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

Pros: Zero JavaScript. Works everywhere. Automatic. Cons: No user toggle. Users can't override their system preference per-site.

Approach 2: CSS Variables + Class Toggle

Add a manual toggle while still supporting system preference.

/* Light theme (default) */
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --surface: #f5f5f5;
  --border: #e0e0e0;
}

/* Dark theme */
:root.dark {
  --bg: #0a0a0a;
  --text: #e5e5e5;
  --surface: #1a1a1a;
  --border: #2a2a2a;
}
// Theme toggle
function toggleTheme() {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
}

// On page load: check saved preference, then system preference
function initTheme() {
  const saved = localStorage.getItem('theme');
  if (saved) {
    document.documentElement.classList.toggle('dark', saved === 'dark');
  } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    document.documentElement.classList.add('dark');
  }
}

initTheme();

Pros: User control. Persists across visits. Falls back to system preference. Cons: Flash of light theme on page load (FOUC) if JavaScript loads late.

Solving the Flash (FOUC)

The biggest dark mode problem: the page loads white, then snaps to dark. Users hate this.

Solution: Inline Script in <head>

Add a blocking script before any CSS renders:

<head>
  <script>
    (function() {
      var saved = localStorage.getItem('theme');
      if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark');
      }
    })();
  </script>
  <!-- CSS loads after this -->
</head>

This runs synchronously before the page renders, so the correct theme is applied immediately.

Next.js Solution

next-themes handles FOUC automatically by injecting a script into <head>:

import { ThemeProvider } from 'next-themes'

// In your root layout
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
  {children}
</ThemeProvider>

Approach 3: Tailwind CSS Dark Mode

Tailwind makes dark mode straightforward with the dark: variant.

// tailwind.config.ts
export default {
  darkMode: 'class', // or 'media' for system-only
}
<div class="bg-white dark:bg-slate-900">
  <h1 class="text-gray-900 dark:text-white">Title</h1>
  <p class="text-gray-600 dark:text-gray-300">Content</p>
  <button class="bg-blue-600 dark:bg-blue-500 text-white">Action</button>
</div>

Tips for Tailwind dark mode:

  • Use semantic color names in your config for consistency
  • Consider a theme config object to avoid repeating dark: everywhere
  • Use @apply in component classes for frequently repeated patterns

Design Guidelines

Color Choices

Don't just invert colors. Dark mode needs intentional design:

ElementLightDarkNotes
Background#ffffff#0a0a0a or #121212Pure black (#000) is too harsh
Surface#f5f5f5#1a1a1a or #1e1e1eCards, modals
Text (primary)#1a1a1a#e5e5e5Not pure white (#fff)
Text (secondary)#6b7280#9ca3afSlightly lighter than light mode
Borders#e5e7eb#2d2d2dSubtle separation
Primary accent#2563eb#60a5faLighter in dark mode

Common Mistakes

  1. Pure black background. Use #0a0a0a or #121212 instead of #000000. Pure black creates too much contrast and causes eye strain.

  2. Pure white text. Use #e5e5e5 or #f0f0f0 instead of #ffffff. Slightly off-white is easier to read on dark backgrounds.

  3. Same shadows. Dark mode shadows should be darker/more subtle. Consider using lighter borders instead of shadows in dark mode.

  4. Forgetting images. Bright images can be jarring in dark mode. Consider reducing brightness slightly:

.dark img { filter: brightness(0.9); }
  1. Ignoring contrast ratios. Both themes need WCAG AA contrast (4.5:1 for body text). Test with a contrast checker.

Three-Way Toggle (Light / Dark / System)

Many users want three options: light, dark, and auto (follow system).

type Theme = 'light' | 'dark' | 'system';

function setTheme(theme: Theme) {
  localStorage.setItem('theme', theme);
  
  if (theme === 'system') {
    const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    document.documentElement.classList.toggle('dark', isDark);
  } else {
    document.documentElement.classList.toggle('dark', theme === 'dark');
  }
}

// Listen for system preference changes (when set to 'system')
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (localStorage.getItem('theme') === 'system') {
    document.documentElement.classList.toggle('dark', e.matches);
  }
});

Framework-Specific Guides

Next.js (App Router)

Use next-themes. It handles SSR, FOUC prevention, and system preference automatically.

Astro

Use the <script> approach in your base layout's <head>. Astro's island architecture means you can use any UI framework's toggle component.

Vue / Nuxt

Use @vueuse/core's useColorMode composable. It handles persistence and system detection.

Vanilla HTML/CSS

Use Approach 2 (CSS variables + class toggle) with the FOUC-preventing inline script.

Testing Dark Mode

  1. Visual inspection. Switch between modes and check every page
  2. Contrast ratios. Use Chrome DevTools accessibility panel
  3. System preference. Test with OS dark mode toggle
  4. Persistence. Reload the page — does it remember your choice?
  5. No flash. Hard-refresh (Cmd+Shift+R) — does the correct theme load instantly?
  6. Images and media. Do images, videos, and embeds look appropriate in both modes?

FAQ

Should dark mode be the default?

Default to system preference (prefers-color-scheme). This respects the user's existing choice without forcing either mode.

Does dark mode affect SEO?

No. Dark mode is purely visual (CSS). Search engines see the same content regardless of theme.

Does dark mode save battery?

On OLED/AMOLED screens, yes — dark pixels are literally turned off. On LCD screens, no meaningful difference.

How do I handle third-party embeds in dark mode?

Many embeds (YouTube, Twitter, code blocks) have dark mode parameters. For others, use CSS filter: invert(1) as a last resort, or wrap in a light-background container.

The Bottom Line

For most projects in 2026:

  1. Tailwind + next-themes if using Next.js (zero FOUC, minimal code)
  2. CSS variables + class toggle for everything else
  3. Always prevent FOUC with an inline <head> script
  4. Default to system preference with a manual override
  5. Don't use pure black — use #0a0a0a or #121212

Dark mode is a 30-minute feature that makes your site feel 10x more polished.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.