Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build dark mode around semantic color tokens, use prefers-color-scheme as the default, and let visitors override that choice with a persisted toggle. Then check contrast, focus indicators, browser controls, and every component in both themes—not just the page background and body text.

Choose the behavior before writing CSS

A solid dark-mode implementation has three parts: a light and dark palette, a rule for choosing between them, and an optional explicit control for visitors. Keep these concerns separate. Semantic CSS custom properties make the palettes maintainable; the system preference provides a sensible default; and a saved user choice takes precedence when one exists.

  • System preference: Use prefers-color-scheme to follow the visitor’s operating-system or browser preference when they have not chosen a theme on your site. MDN reports broad browser availability for this feature since January 2020: MDN: prefers-color-scheme.
  • Explicit override: Offer a clearly labelled toggle or choice for visitors who want a different theme. Persist only a choice they actually made.
  • Browser integration: Declare supported color schemes so native controls can adapt along with your page.
  • Accessibility: Set and verify foreground/background pairs in both palettes, and keep keyboard focus visible.

Do not mechanically invert every color to create the dark palette. The same role—such as muted text, a border, or an error state—needs an appropriate value in each theme, but the values may not be simple inverses.

Build a token-based light and dark palette

Start with semantic roles rather than names tied to a particular appearance. The tokens below cover the page, a raised surface, text, links, and focus; add roles such as borders and status colors as your interface needs them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --surface: #f4f4f5;
  --text: #171717;
  --muted: #525252;
  --link: #005fcc;
  --focus: #8b5cf6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #111214;
    --surface: #1b1d21;
    --text: #f5f5f5;
    --muted: #c4c7ce;
    --link: #8ab4ff;
    --focus: #c4b5fd;
  }
}

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

.card {
  background: var(--surface);
}

a {
  color: var(--link);
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

The media query means the system preference selects the palette until an explicit site-level override is added. Use the tokens consistently across components; a card, menu, or alert that still has a hard-coded light background will not follow the theme. The color-scheme declaration also gives the browser a scheme to use for native controls. For more on centralized theme colors and this declaration, see web.dev: Theming.

Add a toggle that remembers the visitor’s choice

Put the override on the root element as data-theme="light" or data-theme="dark". When the attribute is absent, the media query remains in charge. The button below reports the current state using aria-pressed and names the action it will take.

<meta name="color-scheme" content="light dark">
<button id="theme-toggle" type="button" aria-pressed="false">
  Use dark mode
</button>
<script>
  const root = document.documentElement;
  const button = document.querySelector('#theme-toggle');
  const saved = localStorage.getItem('theme');

  if (saved === 'light' || saved === 'dark') {
    root.dataset.theme = saved;
  }

  function isDark() {
    return root.dataset.theme === 'dark' ||
      (!root.dataset.theme &&
       matchMedia('(prefers-color-scheme: dark)').matches);
  }

  function update() {
    const dark = isDark();
    button.setAttribute('aria-pressed', String(dark));
    button.textContent = dark ? 'Use light mode' : 'Use dark mode';
  }

  button.addEventListener('click', () => {
    root.dataset.theme = isDark() ? 'light' : 'dark';
    localStorage.setItem('theme', root.dataset.theme);
    update();
  });

  update();
</script>

Apply explicit palettes after the system-preference rules. Replace the comments with the same token assignments used for the light and dark palettes above:

:root[data-theme="light"] {
  color-scheme: light;
  /* Set the light token values here. */
}

:root[data-theme="dark"] {
  color-scheme: dark;
  /* Set the dark token values here. */
}

The script accepts only the two expected stored values, so an unrelated or invalid value does not become an accidental theme. The button’s state and text are updated after the saved choice is applied. If you need the system preference to update live while the page is open, listen for changes to the media query—but only while there is no explicit override. Once a visitor has made a choice, leave that choice in control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a page that renders before JavaScript runs, keep the system-based CSS as a working default. The early color-scheme meta tag can help avoid an unwanted flash while the page loads; MDN documents this use of the tag at MDN: color-scheme meta value.

Consider the modern light-dark() option

CSS can also pair values directly using light-dark(), provided the relevant color scheme is declared:

:root {
  color-scheme: light dark;
  --page: light-dark(#fff, #111214);
  --text: light-dark(#171717, #f5f5f5);
}

For browser audiences that need a fallback, retain the media-query token implementation or define fallback token values before the paired declarations. web.dev reports support in all three major browser engines and marks light-dark() Baseline Newly available as of 13 May 2024; check the browser baseline for your own audience before relying on it: web.dev: light-dark().

Meet contrast and keyboard-accessibility requirements

Dark mode does not lower the contrast bar. WCAG 2.2, published as a W3C Recommendation on 5 October 2023, sets these relevant AA requirements:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
What to evaluate Minimum contrast Standard
Normal-size text 4.5:1 WCAG 2.2 Success Criterion 1.4.3
Large text 3:1 WCAG 2.2 Success Criterion 1.4.3
Visual information identifying relevant controls, states, and meaningful graphics 3:1 WCAG 2.2 Success Criterion 1.4.11

Test the actual foreground against its actual background in each theme. Include links and visited links, placeholders, disabled and read-only controls, borders, dividers, icons, charts, selected rows, alerts, validation messages, focus rings, menus, dialogs, date pickers, code blocks, text over images, SVG fills and strokes, and third-party embeds. A color that works on the page background may fail on a card or tinted alert. Declaring each foreground and background as a pair makes those checks easier to reason about. web.dev describes poor contrast as one of the most common accessibility issues: web.dev: Color and contrast accessibility.

Keep keyboard focus visible in both themes. The :focus-visible rule above gives focused elements an outline, but you still need to confirm the outline stands out against the surfaces around it. WCAG 2.2 includes a Focus Visible requirement; its AAA Focus Appearance criterion describes a 3:1 relationship for focus indicators. Do not rely on a color change alone to convey selection, success, error, or another status: pair color with text, an icon, a pattern, shape, or an accessible name.

Pure black is not automatically the best dark background. Choose page and surface colors that preserve readable contrast and useful visual hierarchy, then evaluate them at typical screen brightness rather than assuming a single palette suits every display.

Review the whole interface in both themes

Use this sequence to avoid shipping a palette that works only on the main content area:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Inventory colors. Assign each current color a semantic role such as --bg, --surface, --text, --muted, --border, --link, --focus, --success, or --error.
  2. Validate the light palette. Check normal text, large text, controls, meaningful graphics, and keyboard focus before treating it as the baseline.
  3. Design the dark palette by role. Choose values for the same tokens; do not assume an automatic color inversion will preserve meaning or hierarchy.
  4. Set the selection and override rules. Follow prefers-color-scheme by default, store only an explicit visitor choice, and ensure the override wins over the media query.
  5. Integrate browser controls and initial rendering. Set color-scheme appropriately and place the scheme meta tag early in the document.
  6. Exercise real interaction states. Navigate by keyboard, use 200% zoom, and review responsive layouts, forced-colors or high-contrast settings, reduced motion, and print styles.
  7. Inspect assets and embedded content. Review screenshots, SVGs, and third-party widgets, which may retain colors outside your tokens.
  8. Run automated checks and a visual pass. Use an accessibility inspector or contrast checker in both themes, then manually review the result at typical brightness levels.

Automated accessibility contrast testing can flag failing color pairs quickly, but a tool cannot decide whether an icon, chart, or state has a clear meaning. Pair checks with a manual review of the full interaction and content states.

Test screenshots of both themes

Capture the same page in light and dark mode, including representative forms, dialogs, menus, alerts, and content-heavy screens. A screenshot helps reveal missed hard-coded colors and components that do not follow the tokens. Make sure your capture setup can render the intended color scheme; if a screenshot tool cannot control or inherit the relevant browser preference, test the page through a browser configuration that can.

For repeatable screenshot checks, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a screenshot or PDF, and its supported options include dark mode, device and viewport settings, custom CSS and JavaScript, selector capture, and waiting for page conditions. See ScreenshotNeo for product information.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Or skip the browser setup

Use the API for a quick capture; this example requests a WebP screenshot of a page in dark mode. See the ScreenshotNeo API documentation for authentication and request options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://stripe.com 
  -d dark_mode=true 
  -o shot.webp

ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and the response identifies the page verdict and billing status. Its MCP server lets AI agents—including Claude, Cursor, and other MCP clients—use screenshot tools. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up free for 1,000 screenshots a month—no card required.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot common dark-mode failures

The toggle label changes, but the page colors do not

Check that the click handler sets data-theme on the same root element targeted by your explicit-theme CSS, and that the component styles use tokens rather than hard-coded colors. Confirm the override selectors are not being superseded by more specific rules or later declarations.

The page follows the system setting but ignores the saved choice

Verify that the saved value is exactly light or dark, that it is read before the UI is updated, and that explicit theme selectors take precedence over the media-query palette. Test both cases: no saved choice, and a saved choice that conflicts with the operating-system setting.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Native inputs remain light or clash with the theme

Declare the supported scheme with color-scheme and set it to light or dark on the explicit-theme selectors. Test inputs, selects, date pickers, and other browser-rendered controls rather than assuming page tokens recolor them.

The first render flashes the wrong scheme

Keep the system-based CSS available without waiting for the toggle script, place the color-scheme meta tag before your CSS, and apply any saved override as early as your rendering setup allows. A late script can still produce a visible transition if the initial document cannot know the saved preference.

Text passes, but icons, borders, or focus rings disappear

Check non-text contrast for meaningful component boundaries, states, and graphics, then test focus against the adjacent surfaces. Give those roles their own tokens and inspect them in both themes instead of reusing a text color indiscriminately.

An embedded widget stays in the wrong theme

First determine whether the widget supports the page’s color scheme or offers its own theme setting. Review the embed in both palettes; your site’s custom properties do not necessarily reach content rendered by a third party.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The theme looks correct on screen but not in print or high-contrast mode

Review print styles, forced-colors or high-contrast settings, and reduced-motion behavior separately. Do not assume the ordinary dark palette should override the user’s accessibility settings.

FAQ

Should dark mode be the default for every visitor?

No. Following the visitor’s system preference by default respects their chosen environment; an explicit site control gives them a way to choose differently.

Should the site save the theme preference?

Save it when the visitor explicitly changes the theme so their override persists. If they have not chosen, leave the preference to the system rather than storing an inferred setting.

Can I use light-dark() without a media query?

It can reduce paired color declarations, but whether it is suitable depends on the browsers your audience needs to support. Keep a fallback when your support requirements include browsers outside the feature’s current baseline.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

SaleBestseller No. 1
HTML and CSS: Design and Build Websites
HTML and CSS: Design and Build Websites
HTML CSS Design and Build Web Sites; Comes with secure packaging; It can be a gift option
$14.94
SaleBestseller No. 3
SaleBestseller No. 4
Web Design with HTML, CSS, JavaScript and jQuery Set
Web Design with HTML, CSS, JavaScript and jQuery Set
Brand: Wiley; Set of 2 Volumes
$35.05

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.