/ CSS Checker Fixes / Dark Mode

How to Fix Dark Mode CSS

Dark mode looks simple — invert the colours, done. Real dark mode is harder: contrast ratios change, brand colours shift, photos need different treatment, and the flash from light to dark on load reveals every implementation flaw. This guide walks through dark mode done right: design tokens, accessible contrast in both modes, image handling, persistence, and no-flash loading.

1. Define your tokens for both modes

Hard-coded colours can't switch modes. Replace every colour value with a CSS variable that has light AND dark definitions.

:root {
  /* Light mode defaults */
  --bg: #ffffff;
  --bg-elevated: #f8fafc;
  --text: #0f172a;
  --text-muted: #475569;
  --border: #e2e8f0;
  --accent: #7c3aed;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --bg-elevated: #1e293b;
    --text: #f1f5f9;
    --text-muted: #94a3b8;
    --border: #334155;
    --accent: #a78bfa;  /* Lighter accent for dark mode */
  }
}

/* All components use tokens, never raw colours */
body { background: var(--bg); color: var(--text); }
.card { background: var(--bg-elevated); border: 1px solid var(--border); }
.muted { color: var(--text-muted); }

This is the core pattern. Everything else builds on it.

2. Don't invert — design

⚠️ Algorithmic colour inversion produces ugly results. Pure black backgrounds (#000) are too harsh; pure white text on pure black is too high-contrast and causes "haloing". Real dark mode uses considered greys.

Typical good dark mode values

--bg: #0f172a;          /* Off-black, not pure black */
--bg-elevated: #1e293b; /* Slightly lighter for cards */
--text: #f1f5f9;        /* Off-white for body text */
--text-muted: #94a3b8;  /* Soft grey for secondary */
--border: #334155;      /* Visible but not loud */

Brand colours in dark mode

Vibrant brand colours often fail contrast in dark mode. Define dark-mode variants:

--accent: #7c3aed;  /* Light mode — passes 4.5:1 on white */

@media (prefers-color-scheme: dark) {
  :root {
    --accent: #a78bfa;  /* Lighter variant — passes 4.5:1 on dark */
  }
}

3. Verify contrast in dark mode

Dark mode pairs need their own WCAG verification. Common failure: dim grey text on dark background fails 4.5:1.

Step 1
Check every text token
For each text colour on each background colour combination, check ratio:
  • --text on --bg: should be 4.5:1+
  • --text-muted on --bg: 4.5:1+ for body text, 3:1+ for large
  • --text on --bg-elevated: same checks for card surfaces
  • --accent on --bg: 4.5:1+ for links
Step 2
Use a contrast tool
WebAIM Contrast Checker or browser DevTools colour picker shows ratio in real time. The Chrome DevTools colour picker displays a horizontal line on the colour gradient showing the AA threshold — colours above it pass, below it fail.

4. Handle images for dark mode

Logos with SVG

<!-- Use currentColor so logo adapts to text colour -->
<svg fill="currentColor">
  <path d="..."/>
</svg>

The SVG inherits the text colour, which already changes with the theme. No separate dark logo needed.

Logos with PNG / multiple variants

<picture>
  <source srcset="/logo-dark.png" 
          media="(prefers-color-scheme: dark)">
  <img src="/logo-light.png" alt="Brand">
</picture>

Photos and illustrations

Most photos work in both modes. Heavily-shadowed dark photos can look "blown out" on light backgrounds; very bright photos can be glaring on dark backgrounds. Test specific images; provide alternatives only for problem cases.

Illustrations with white strokes

Need dark variants. Or convert to SVG with currentColor.

5. User toggle on top of OS preference

Default to OS preference, allow user override. Three-state toggle works best: System, Light, Dark.

// Markup
<button id="theme-toggle" aria-label="Toggle theme">
  <span class="theme-icon"></span>
</button>

// JS — apply chosen theme via data attribute
const stored = localStorage.getItem('theme') || 'system';
applyTheme(stored);

function applyTheme(mode) {
  if (mode === 'system') {
    document.documentElement.removeAttribute('data-theme');
  } else {
    document.documentElement.setAttribute('data-theme', mode);
  }
  localStorage.setItem('theme', mode);
}

document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = localStorage.getItem('theme') || 'system';
  const next = current === 'system' ? 'light' 
             : current === 'light' ? 'dark' 
             : 'system';
  applyTheme(next);
});

CSS handles the data-theme override

:root { /* light defaults */ }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* dark values — applies when OS is dark AND user hasn't overridden to light */
  }
}

:root[data-theme="dark"] {
  /* dark values — applies when user explicitly chose dark */
}

6. Prevent the flash

⚠️ The "flash of incorrect theme" (FOIT) happens when JS to apply dark mode runs AFTER the browser has painted the page in light mode. Fix: apply theme synchronously in the head, before any rendering.
<head>
  <script>
    // INLINE in head — runs before any render
    (function() {
      const stored = localStorage.getItem('theme');
      if (stored === 'dark') {
        document.documentElement.setAttribute('data-theme', 'dark');
      } else if (stored === 'light') {
        document.documentElement.setAttribute('data-theme', 'light');
      }
      // 'system' or null: let prefers-color-scheme media query work
    })();
  </script>
  <link rel="stylesheet" href="/main.css">
</head>

This blocking script runs before CSS parses, so the data-theme attribute is set before any styles apply. No flash.

7. Test thoroughly

Step 1
Both OS modes
Test with system dark mode on AND off. Reload pages with each setting. Confirm no flash on either. Confirm contrast holds on both.
Step 2
User toggle override
Set OS to light, force dark via toggle, reload. App should remain dark. Same in reverse. Toggle to System and reload — should follow OS.
Step 3
Edge cases
Test: form inputs (browser default styles differ in dark mode), images with transparent PNG, video posters, embedded iframes (Twitter, YouTube — they have their own theme handling).
💡 Dark mode quality is a brand signal. Apple, Apple-adjacent products, developer tools, and most modern SaaS expect polished dark mode. Get it right and users notice. Get it wrong (flash, broken contrast, missing logos) and the rough edges feel cheap.

🎨 Re-run the CSS Checker

Verify dark-mode findings are cleared.

Run CSS Checker →
Related Guides: CSS Checker Fixes  ·  Fix Colour Contrast  ·  Fix Layout Shift CSS  ·  CSS Checker Guide
💬 Got a problem?