1. prefers-color-scheme Media Query

The @media (prefers-color-scheme: dark) query automatically detects the user's operating system theme without JavaScript. This is the minimum baseline for dark mode support.

Light vs Dark (OS detected)

Left = light theme, Right = dark theme. The media query switches styling automatically.

Your current OS preference
Detecting…

This page reads your system preference using matchMedia.

/* Basic dark mode with media query */
body {
  background: #ffffff;
  color: #111827;
}

@media (prefers-color-scheme: dark) {
  body {
    background: #0f172a;
    color: #e2e8f0;
  }
}

/* Check in JavaScript */
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
console.log(prefersDark); // true or false

/* Listen for OS theme changes */
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', e => {
    console.log(e.matches ? 'dark' : 'light');
  });

2. CSS Variables: The Right Approach

Hardcoding colors in a media query quickly becomes messy. The professional approach is to define all colors as CSS custom properties in :root, then override them for dark mode. This keeps your component styles theme-agnostic.

Light theme
Dashboard LIGHT
View Report →

Light palette: white background, dark text, indigo accent.

Dark theme
Dashboard DARK
View Report →

Dark palette: deep navy background, soft text, brighter indigo accent.

Palette comparison

Light → Dark

bg
bg
surface
surface
text
text
accent
accent

Each value is a CSS variable. Swap the values, not every rule.

/* 1. Define variables in :root (light = default) */
:root {
  --color-bg:      #ffffff;
  --color-surface: #f3f4f6;
  --color-text:    #111827;
  --color-muted:   #6b7280;
  --color-accent:  #6366f1;
  --color-border:  #e5e7eb;
}

/* 2. Override for dark mode */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg:      #0f172a;
    --color-surface: #1e293b;
    --color-text:    #e2e8f0;
    --color-muted:   #94a3b8;
    --color-accent:  #818cf8;
    --color-border:  #334155;
  }
}

/* 3. Use variables everywhere (no theme logic needed in components) */
body          { background: var(--color-bg);      color: var(--color-text); }
.card         { background: var(--color-surface);  border: 1px solid var(--color-border); }
.button       { background: var(--color-accent); }
.label        { color: var(--color-muted); }
Tip: Avoid repeating raw color values in component styles. If a color ever needs to change, you only update it in :root.

3. JavaScript Toggle with data-theme

A user-controlled toggle lets people override their OS setting. The cleanest pattern is to set a data-theme attribute on the <html> element and scope CSS variable overrides to it. The toggle below controls this entire page. Try it!

Live toggle (affects this page)
My App
Welcome back 👋
Your dashboard looks great in both light and dark mode.
CSS JavaScript

Click the toggle to switch modes. Notice how the preview card above animates smoothly.

How data-theme works
<html data-theme = "dark" >
↑ toggle switches this

<html data-theme = "light" >

JavaScript only changes one attribute on <html>. CSS does all the heavy lifting.

/* CSS: scope dark vars to [data-theme="dark"] */
:root {
  --color-bg:   #ffffff;
  --color-text: #111827;
}

[data-theme="dark"] {
  --color-bg:   #0f172a;
  --color-text: #e2e8f0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  transition: background 0.3s ease, color 0.3s ease;
}
// JavaScript toggle
const toggleBtn = document.getElementById('theme-toggle');
const html = document.documentElement; // <html> element

toggleBtn.addEventListener('click', () => {
  const current = html.getAttribute('data-theme');
  html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
});

4. Remembering the Preference with localStorage

Without persistence, theme choice resets on every page load. Use localStorage to save the user's choice and re-apply it instantly (before the page renders) to avoid a flash of the wrong theme.

Page load flow
1
Read localStorage.getItem('theme')
2
Fall back to prefers-color-scheme
3
Set data-theme on <html>
4
On toggle: save new value to localStorage

Step 1–3 happens in a tiny <script> in <head> before anything renders, preventing a flash.

localStorage API
localStorage .setItem( 'theme' , 'dark' )
localStorage .getItem( 'theme' ) // 'dark' | 'light' | null
localStorage .removeItem( 'theme' )

The stored value persists across page reloads and browser sessions.

/* =========================================================
   Complete dark mode implementation. Copy this entire block.
   ========================================================= */

/* --- CSS in <head> --- */
:root {
  --color-bg:      #ffffff;
  --color-surface: #f3f4f6;
  --color-text:    #111827;
  --color-accent:  #6366f1;
  --color-border:  #e5e7eb;
}

[data-theme="dark"] {
  --color-bg:      #0f172a;
  --color-surface: #1e293b;
  --color-text:    #e2e8f0;
  --color-accent:  #818cf8;
  --color-border:  #334155;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  transition: background 0.3s ease, color 0.3s ease;
}
/* --- Inline script in <head> (BEFORE any other content) ---
   Runs before body renders → no flash of wrong theme */
(function () {
  const saved = localStorage.getItem('theme');
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const theme = saved ?? (prefersDark ? 'dark' : 'light');
  document.documentElement.setAttribute('data-theme', theme);
})();
// --- Toggle button script (can be in <body>) ---
const btn  = document.getElementById('theme-toggle');
const html = document.documentElement;

btn.addEventListener('click', () => {
  const next = html.getAttribute('data-theme') === 'dark'
    ? 'light'
    : 'dark';

  html.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);

  // Optional: update button label/icon
  btn.textContent = next === 'dark' ? 'Light mode' : 'Dark mode';
});

5. Best Practices & Tips

Getting dark mode right goes beyond flipping colors. Here are the most common pitfalls and how to avoid them.

✅ Smooth transitions
body {
  transition: background 0.3s, color 0.3s;
}

Add transition on body and key elements so the switch feels polished instead of abrupt.

✅ Use semantic color names
❌ --blue-500: #6366f1
✅ --color-accent: #6366f1
❌ --gray-100: #f3f4f6
✅ --color-surface: #f3f4f6

Name variables by their role (accent, surface, muted) not their appearance, so dark mode overrides make semantic sense.

✅ Don't forget images & icons
🌄 🌄

Normal vs filter: invert(1)

Icons and illustrations may need separate dark variants or a CSS filter: invert(1) to remain readable.

✅ Respect prefers-reduced-motion
@media (prefers-reduced-motion) {
  body { transition: none; }
}

Skip the smooth transition for users who prefer reduced motion. Accessibility matters.

✅ No FOUT: script in <head>
<head>
  <link rel="stylesheet".../>
  <!-- theme script before body -->
  <script></script>
</head>

Place the theme-init script inside <head>, after CSS, to prevent a flash of unstyled/wrong-theme content (FOUT).

✅ Three-state toggle: light / dark / auto
Light
Dark
Auto

A three-state toggle (Light / Dark / System) gives users the most control. Store "light", "dark", or null (auto).

/* Full recommended implementation: CSS */
:root {
  --color-bg:      #ffffff;
  --color-surface: #f3f4f6;
  --color-text:    #111827;
  --color-accent:  #6366f1;
  --color-border:  #e5e7eb;
}

/* User-chosen dark theme */
[data-theme="dark"] {
  --color-bg:      #0f172a;
  --color-surface: #1e293b;
  --color-text:    #e2e8f0;
  --color-accent:  #818cf8;
  --color-border:  #334155;
}

/* OS dark, no saved preference (data-theme="auto") */
[data-theme="auto"] {
  @media (prefers-color-scheme: dark) {
    --color-bg:      #0f172a;
    --color-surface: #1e293b;
    --color-text:    #e2e8f0;
    --color-accent:  #818cf8;
    --color-border:  #334155;
  }
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  transition: background 0.3s ease, color 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
  body { transition: none; }
}
// Full recommended implementation: JavaScript

// --- In <head>, before body renders ---
(function () {
  const saved = localStorage.getItem('theme') ?? 'auto';
  document.documentElement.setAttribute('data-theme', saved);
})();

// --- Toggle button (3 states) ---
const themes = ['light', 'dark', 'auto'];
let idx = themes.indexOf(document.documentElement.getAttribute('data-theme'));

document.getElementById('theme-toggle').addEventListener('click', () => {
  idx = (idx + 1) % themes.length;
  const next = themes[idx];
  document.documentElement.setAttribute('data-theme', next);

  if (next === 'auto') {
    localStorage.removeItem('theme');  // let OS decide
  } else {
    localStorage.setItem('theme', next);
  }
});