Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 6 – Colour and theming

Dark mode, colour schemes and forced colours

Support light and dark themes with color-scheme, prefers-color-scheme and light-dark(), add a theme switch, and keep pages usable in forced colours.

  • Intermediate
  • 30 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Support light and dark themes with color-scheme, prefers-color-scheme and light-dark()
  • Add a user theme switch that overrides the system preference without duplicating rules
  • Check every colour pair of a theme in both schemes
  • Design pages that stay usable in forced-colours (high-contrast) mode

Before you start

What you’ll learn · Before you start

What you will learn

  • Support light and dark themes with color-scheme, prefers-color-scheme and light-dark()
  • Add a user theme switch that overrides the system preference without duplicating rules
  • Check every colour pair of a theme in both schemes
  • Design pages that stay usable in forced-colours (high-contrast) mode

Before you start

On this page

Dark mode takes three pieces. The user’s setting reaches the page as the prefers-color-scheme media feature, light or dark. The page says which schemes it supports with the color-scheme property, usually color-scheme: light dark on the root element, or with a <meta name="color-scheme"> element; that one declaration lets the browser draw form controls, scrollbars, the page background and its system colours in the dark scheme. Your own colours then switch with light-dark(), which takes a light and a dark value, or with rules inside @media (prefers-color-scheme: dark). A theme menu that overrides the system needs only one change: set color-scheme on the root to light, dark or light dark. Forced colours mode, which the contrast themes of Windows turn on, goes further: it replaces the page’s colours with the user’s own palette, so a page must stay usable without its colours.

This lesson measures each piece in Chromium 153, Firefox 155 and WebKit 26.6 with light, dark and forced colours emulated, and reproduces the two mistakes that break most dark themes.

color-scheme: what the page supports

color-scheme takes normal, the default, or a list of the schemes an element is designed for: light, dark, or both. When the user’s preferred scheme is in the list, the element uses it; otherwise it uses the first scheme listed; with normal and nothing else on the page, it supports no scheme and gets the browser’s default, which is light. The keyword only, as in only light, also forbids the browser from forcing its own automatic dark version on the element. The property is inherited, so :root { color-scheme: light dark; } covers the whole page, and a part of the page can still opt out with its own value.

The used scheme changes what the browser draws for you: the default colours of form controls, scrollbars and other built-in parts, and, on the root, the page’s canvas behind everything. It also changes the system colours. This page reads them in two panels, one with color-scheme: light and one with color-scheme: dark:

System colours in the light and the dark scheme HTML · system-colours.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>System colours in the light and the dark scheme</title>
  <style>
    body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
    .light { color-scheme: light; }
    .dark { color-scheme: dark; }
    .panel { padding: 12px; margin-block-end: 12px; background-color: Canvas; color: CanvasText; }
  </style>
</head>
<body>
  <div class="panel light"><input value="A field in the light scheme"> <a href="#top">a link</a></div>
  <div class="panel dark"><input value="A field in the dark scheme"> <a href="#top">a link</a></div>
  <script>
    const keywords = ['Canvas', 'CanvasText', 'LinkText', 'Field', 'FieldText', 'ButtonFace', 'ButtonText', 'GrayText', 'Highlight'];
    for (const panel of document.querySelectorAll('.panel')) {
      const probe = document.createElement('span');
      panel.append(probe);
      const values = keywords.map((k) => {
        probe.style.color = k;
        return `${k} ${getComputedStyle(probe).color}`;
      });
      probe.remove();
      const field = getComputedStyle(panel.querySelector('input'));
      console.log(`${panel.className.replace('panel ', '')}: ${values.join(', ')}`);
      console.log(`  the input: background ${field.backgroundColor}, text ${field.color}`);
    }
  </script>
</body>
</html>

Under color-scheme: dark, the three engines reported:

  • Canvas: rgb(18, 18, 18) in Chromium 153, rgb(28, 27, 34) in Firefox 155, rgb(30, 30, 30) in WebKit 26.6.
  • CanvasText: rgb(255, 255, 255), rgb(251, 251, 254) and rgb(255, 255, 255).
  • LinkText: rgb(158, 158, 255), rgb(0, 202, 219) and rgb(158, 158, 255).
  • Field: rgb(59, 59, 59), rgb(43, 42, 51) and the translucent rgba(255, 255, 255, 0.247).

In the light panel all three engines agreed on a white Canvas, black CanvasText and the classic link blue rgb(0, 0, 238). In the dark panel each engine used its own palette, and the input turned dark in all three. That is the point of system colours: use them where something should look like the browser’s own interface, and set your own colours wherever a design needs exact values.

A stylesheet loads after the HTML has started to render, so a page that sets color-scheme only in CSS can flash white for a moment in dark mode. The HTML Standard’s answer is the meta element, which the browser reads before any CSS: <meta name="color-scheme" content="light dark">. Use it together with the CSS property. Both are Baseline widely available.

light-dark() follows the scheme, not the media query

light-dark(#ffffff, #1c1f26) computes to its first colour where the element’s used scheme is light and to its second where it is dark. It does not read prefers-color-scheme itself, which is where most attempts to use it go wrong:

light-dark() with and without color-scheme HTML · light-dark-card.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>light-dark() with and without color-scheme</title>
  <style>
    body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
    .card {
      --surface: light-dark(#ffffff, #1c1f26);
      --text: light-dark(#1a1a1a, #e8eaed);
      --muted: light-dark(#5f6368, #a8adb5);
      --border: light-dark(#8a8f98, #6b717c);
      --accent: light-dark(#0044cc, #8ab4ff);
      padding: 12px;
      margin-block-end: 12px;
      color: var(--text);
      background-color: var(--surface);
      border: 1px solid var(--border);
    }
    .card small { color: var(--muted); }
    .card a { color: var(--accent); }
    /* light-dark() picks the dark value only where the used colour scheme is dark. */
    .supports-both { color-scheme: light dark; }
  </style>
</head>
<body>
  <div class="card" id="default">Card A, no color-scheme <small>last sync 2 min ago</small> <a href="#top">Details</a></div>
  <div class="supports-both">
    <div class="card" id="both">Card B, inside color-scheme: light dark <small>last sync 2 min ago</small> <a href="#top">Details</a></div>
  </div>
  <script>
    console.log(`prefers dark: ${matchMedia('(prefers-color-scheme: dark)').matches}`);
    for (const card of document.querySelectorAll('.card')) {
      const s = getComputedStyle(card);
      const muted = getComputedStyle(card.querySelector('small')).color;
      const accent = getComputedStyle(card.querySelector('a')).color;
      console.log(`${card.id.padEnd(7)} surface ${s.backgroundColor}, text ${s.color}, muted ${muted}, accent ${accent}`);
    }
  </script>
</body>
</html>

With the system set to dark, card A, which sits on a page with no color-scheme, stayed white with dark text in all three engines: its used scheme was the light default. Card B, inside an element with color-scheme: light dark, switched to the surface rgb(28, 31, 38) and the text rgb(232, 234, 237). With the system set to light, both cards were light. One declaration on the root is the whole fix.

light-dark() for colours is Baseline newly available. A newer form also accepts two images, such as two gradients, and is newly available in the latest browsers only; where older browsers matter, put a plain declaration before the one with light-dark(), and they keep it.

A theme needs every colour pair checked twice, once per scheme, because a colour that works on one surface can fail on the other. The program below checks the card’s tokens:

Every colour pair of the card, in both schemes JavaScript · token_contrast.mjs
// Every colour pair of the dashboard card in light-dark-card.html, checked in both schemes against WCAG 2.2:
// 4.5:1 for text and 3:1 for the border, which shows where the card ends (1.4.11). The last row reuses the light
// theme's accent in the dark theme, a common mistake when a dark theme is added later.

const tokens = {
  surface: ['#ffffff', '#1c1f26'],
  text: ['#1a1a1a', '#e8eaed'],
  muted: ['#5f6368', '#a8adb5'],
  border: ['#8a8f98', '#6b717c'],
  accent: ['#0044cc', '#8ab4ff'],
};

const luminance = (hex) => {
  const lin = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
  return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
};
const contrast = (a, b) => {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
};

const checks = [
  ['text on surface', 'text', 4.5],
  ['muted on surface', 'muted', 4.5],
  ['accent on surface', 'accent', 4.5],
  ['border on surface', 'border', 3],
];

console.log('pair                  light   dark  target');
for (const [label, token, target] of checks) {
  const [light, dark] = [0, 1].map((i) => contrast(tokens[token][i], tokens.surface[i]));
  const verdict = light >= target && dark >= target ? 'pass' : 'FAIL';
  console.log(`${label.padEnd(20)}  ${light.toFixed(2).padStart(5)}  ${dark.toFixed(2).padStart(5)}  ${String(target).padStart(6)}  ${verdict}`);
}
const reused = contrast(tokens.accent[0], tokens.surface[1]);
console.log(`light accent on the dark surface: ${reused.toFixed(2)} ${reused >= 4.5 ? 'pass' : 'FAIL'}`);

Output

pair                  light   dark  target
text on surface       17.40  13.68     4.5  pass
muted on surface       6.05   7.31     4.5  pass
accent on surface      7.78   7.89     4.5  pass
border on surface      3.25   3.36       3  pass
light accent on the dark surface: 2.12 FAIL

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node token_contrast.mjs

Every pair passes in both schemes, the border at 3.25:1 and 3.36:1 against the 3:1 that marks where the card ends. The last line is the mistake that turns up when a dark theme is added to an existing site: the light theme’s link blue on the dark surface reaches only 2.12:1. Dark themes need their own, lighter accent colours.

The user preference, the color-scheme the page declares and forced colours mode together give the used scheme, which light-dark() follows.Three inputsThe element's used colour schemelight or darklight-dark() picks its valuesystem colours · form controlsscrollbars · the page canvasThe user's settingprefers-color-scheme: light or darkWhat the page supportscolor-scheme (or the meta element)Forced colours mode on?Then color-scheme becomes light darkand the user's palette replaces colours

Three inputs decide the used colour scheme, and four things follow it

Text description of the diagram

The diagram shows three boxes feeding one result.

  1. The user's setting, which pages read as the prefers-color-scheme media feature: light or dark.
  2. What the page says it supports, with the color-scheme property or the meta element of the same name. Without either, an element supports no scheme and gets the browser's default, light.
  3. Forced colours mode. When it is on, color-scheme computes to light dark and the user's palette replaces the page's colours.

Together they give the element's used colour scheme, light or dark: the preferred scheme if the element supports it, otherwise the first scheme it lists. Four things follow the used scheme: which value light-dark() picks, the values of system colours such as Canvas and Field, the colours of form controls and scrollbars, and the background of the page canvas.

A theme switch without duplicated rules

Some readers want a different theme from their system’s. Because light-dark(), form controls and system colours all follow the used scheme, a theme menu only has to change color-scheme on the root element: light or dark to choose one, light dark to follow the system again. Nothing else in the stylesheet changes:

A theme menu that only changes color-scheme HTML · theme-switch.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <!-- Says before any CSS loads that the page supports both schemes, so the first paint can already be dark. -->
  <meta name="color-scheme" content="light dark">
  <title>A theme menu that only changes color-scheme</title>
  <style>
    :root { color-scheme: light dark; }
    body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
    .card {
      padding: 12px;
      margin-block: 12px;
      color: light-dark(#1a1a1a, #e8eaed);
      background-color: light-dark(#ffffff, #1c1f26);
    }
    /* A rule written with the media query follows the operating system, never the menu. */
    .banner { padding: 8px; background-color: #e8f0fe; }
    @media (prefers-color-scheme: dark) {
      .banner { background-color: #263238; }
    }
  </style>
</head>
<body>
  <label for="theme">Theme</label>
  <select id="theme">
    <option value="system">System</option>
    <option value="light">Light</option>
    <option value="dark">Dark</option>
  </select>
  <div class="card">Dashboard card</div>
  <div class="banner">Banner styled with a media query</div>
  <script>
    // One change for the whole page: color-scheme on the root element. "System" lets the browser follow the
    // operating system again. A real site would also save the choice; this demo keeps it in memory only.
    const schemes = { system: 'light dark', light: 'light', dark: 'dark' };
    const menu = document.getElementById('theme');
    const report = () => {
      const card = getComputedStyle(document.querySelector('.card'));
      const banner = getComputedStyle(document.querySelector('.banner'));
      const field = getComputedStyle(menu);
      console.log(`${menu.value.padEnd(6)} card ${card.backgroundColor} on text ${card.color}; menu ${field.backgroundColor}; banner ${banner.backgroundColor}`);
    };
    menu.addEventListener('change', () => {
      document.documentElement.style.colorScheme = schemes[menu.value];
      requestAnimationFrame(report);
    });
    console.log(`prefers dark: ${matchMedia('(prefers-color-scheme: dark)').matches}`);
    report();
  </script>
</body>
</html>

The page was loaded with the system set to light and then to dark, and each menu choice was made in turn. All three engines behaved the same way:

  • System light, menu on System or Light: the card was white and the menu field light; the banner from the media query was light, rgb(232, 240, 254).
  • System light, menu on Dark: the card turned rgb(28, 31, 38) and the menu field dark; the banner stayed light.
  • System dark, menu on System or Dark: the card was rgb(28, 31, 38) and the menu field dark; the banner was dark, rgb(38, 50, 56).
  • System dark, menu on Light: the card was white and the menu field light; the banner stayed dark.

The card and the native select followed the menu. The banner, written with @media (prefers-color-scheme: dark), followed only the system, because that media feature reports the user’s setting and knows nothing about the page’s menu. A site that offers a theme switch should write its colours with light-dark(); with media queries it would have to repeat every dark rule under a class or an attribute set by the menu, which is the duplication this approach avoids.

A real site also remembers the choice, usually in localStorage, and applies it before the first paint, with a small script in the page’s head or with the value written into the HTML by the server, so returning readers never see the wrong theme flash. The HTML, CSS & JS Playground runs your code in an isolated frame where localStorage is not available, so the demo keeps the choice in memory only. Without any script, radio buttons and :has() can do the same: :root:has(#theme-dark:checked) { color-scheme: dark; }, but nothing will remember the choice.

HTML, CSS & JS Playground (Online Editor) Paste theme-switch.html, switch your system between light and dark, and try each menu choice.

Forced colours mode

Forced colours mode is an accessibility setting: the user picks a palette, often of high contrast, and the browser applies it to every page. Pages can detect it with @media (forced-colors: active), Baseline widely available, and the palette is available to them through the system colours. CSS Color Adjustment Level 1 lists what changes. The colour properties, such as color, background-color, border-color, outline-color, fill and stroke, are given system colours, box-shadow and text-shadow compute to none, background-image computes to none unless it holds a url() image, color-scheme computes to light dark, and accent-color and scrollbar-color go back to auto.

A status list and a button in forced colours mode HTML · forced-colours.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A status list and a button in forced colours mode</title>
  <style>
    body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
    .status { display: inline-block; padding: 2px 10px; border-radius: 999px; color: white; }
    .status.ok { background-color: #1e7e34; }
    .status.down { background-color: #c62828; }
    /* Without this border, the two chips differ only in background colour. */
    .status { border: 1px solid transparent; }
    .status .icon { margin-inline-end: 4px; }

    .action {
      padding: 8px 16px;
      font: inherit;
      color: white;
      background-color: #0044cc;
      border: 2px solid transparent;
      border-radius: 6px;
    }
    .action:focus-visible {
      outline: 2px solid transparent;  /* becomes a visible system colour when colours are forced */
      box-shadow: 0 0 0 4px #f26b1d;   /* removed when colours are forced */
    }
    @media (forced-colors: active) {
      .status.down { border-style: dashed; } /* a second cue besides colour */
      .action:focus-visible { outline-offset: 2px; }
    }
  </style>
</head>
<body>
  <span class="status ok"><span class="icon" aria-hidden="true">✓</span>Online</span>
  <span class="status down"><span class="icon" aria-hidden="true">✕</span>Offline</span>
  <button class="action" type="button">Restart</button>
  <script>
    const forced = matchMedia('(forced-colors: active)').matches;
    console.log(`forced colours: ${forced}, root color-scheme ${getComputedStyle(document.documentElement).colorScheme}`);
    for (const chip of document.querySelectorAll('.status')) {
      const s = getComputedStyle(chip);
      console.log(`${chip.textContent.padEnd(8)} background ${s.backgroundColor}, text ${s.color}, border ${s.borderTopColor} ${s.borderTopStyle}`);
    }
    const button = document.querySelector('.action');
    button.focus({ focusVisible: true });
    const b = getComputedStyle(button);
    console.log(`focused button: outline ${b.outlineColor} ${b.outlineStyle} ${b.outlineWidth}, box-shadow ${b.boxShadow}`);
  </script>
</body>
</html>

The page was loaded with Playwright’s forced colours emulation. In Chromium and Firefox:

  • Both status chips got a white background and black text: the green and the red were gone. What still told them apart were the words, the icons, and the dashed border that the forced-colors rule gives the offline chip. Colour must never be the only way to show a state, which is also what WCAG 2.2’s success criterion 1.4.1 asks.
  • The transparent borders became visible system colours, rgb(0, 0, 0) on the light palette, so the chips kept their outlines.
  • The focus ring drawn with box-shadow computed to none, but the transparent outline beside it got a visible colour, rgba(5, 0, 73, 0.8) in Chromium and rgb(0, 0, 0) in Firefox, so keyboard users could still see focus.
  • color-scheme on the root computed to light dark in Chromium, as the specification says; Firefox still reported normal.

WebKit matched the emulated forced-colors: active query, so the dashed border appeared, but it replaced no colour at all. Test this mode in Chromium or Firefox, in DevTools’ rendering settings or with Playwright, and on Windows with a contrast theme before a release.

Write rules for this mode sparingly, and only with system colours: Highlight for a selected item or ButtonText for an icon, for example. forced-color-adjust: none takes an element out of the mode entirely and is meant for content whose colour is the information, such as a colour swatch; it has limited availability, because Safari doesn’t support it. In forced colours mode browsers also match prefers-contrast: more or less when the user’s palette is clearly of high or low contrast (custom otherwise), and the matching prefers-color-scheme value when it is clearly dark or light.

Practise: the colour scheme a browser uses

Write the rules that decide the used colour scheme, the value a theme menu sets, and what forced colours mode removes:

Exercise · Medium · JavaScript

Work out the colour scheme a browser uses, and what forced colours remove

light-dark(), form controls and system colours all follow the used colour scheme of an element, and a theme switch works only if you know how the browser picks it. Write three functions in theme.mjs and export them, following CSS Color Adjustment Level 1.

usedScheme(value, preferred, pageSchemes = null) returns 'light' or 'dark'. value is the element's computed color-scheme, such as 'normal', 'dark', 'light dark' or 'only light'; preferred is the user's prefers-color-scheme, 'light' or 'dark'; pageSchemes is the content of the page's <meta name="color-scheme">, or null when there is none.

- normal means the page's supported schemes from the meta element; without one, the element supports no scheme. - Only light and dark count. only and any other word are ignored. - If the preferred scheme is in the list, use it; otherwise use the first scheme in the list; with an empty list, use the browser's default, 'light'.

themeValue(choice) turns the choice of a theme menu into the value to set on the root element's color-scheme: 'system' gives 'light dark', 'light' gives 'light' and 'dark' gives 'dark'. Anything else throws a RangeError.

forcedComputed(property, value) returns a property's computed value once forced colours mode is on, for the properties the specification changes: box-shadow and text-shadow become 'none', background-image becomes 'none' unless its value contains a url( image, color-scheme becomes 'light dark', and accent-color and scrollbar-color become 'auto'. Every other property keeps its value; colours are replaced later, when the browser paints.

The sample tests import the three functions from theme.mjs and run in your browser.

Starter code · theme.mjs

/** The used colour scheme, 'light' or 'dark', from color-scheme, the user's preference and the page's meta element. */
export function usedScheme(value, preferred, pageSchemes = null) {
  // Replace this line with your code.
  return 'light';
}

/** The color-scheme value a theme menu sets on the root element for 'system', 'light' or 'dark'. */
export function themeValue(choice) {
  // Replace this line with your code.
  return 'normal';
}

/** A property's computed value in forced colours mode (CSS Color Adjustment 1, section 3.1). */
export function forcedComputed(property, value) {
  // Replace this line with your code.
  return value;
}
The sample tests · theme.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { usedScheme, themeValue, forcedComputed } from './theme.mjs';

test('the preferred scheme wins when the element supports it', () => {
  assert.equal(usedScheme('light dark', 'dark'), 'dark');
  assert.equal(usedScheme('light dark', 'light'), 'light');
  assert.equal(usedScheme('dark light', 'light'), 'light');
});

test('otherwise the first scheme the element lists', () => {
  assert.equal(usedScheme('dark', 'light'), 'dark');
  assert.equal(usedScheme('light', 'dark'), 'light');
  assert.equal(usedScheme('only light', 'dark'), 'light');
});

test('normal follows the meta element, or the browser default without one', () => {
  assert.equal(usedScheme('normal', 'dark'), 'light');
  assert.equal(usedScheme('normal', 'dark', 'light dark'), 'dark');
  assert.equal(usedScheme('normal', 'light', 'dark'), 'dark');
});

test('words other than light and dark are ignored', () => {
  assert.equal(usedScheme('light sepia', 'dark'), 'light');
  assert.equal(usedScheme('sepia', 'dark'), 'light');
  assert.equal(usedScheme('sepia dark', 'light'), 'dark');
});

test('a theme menu sets one value on the root element', () => {
  assert.equal(themeValue('system'), 'light dark');
  assert.equal(themeValue('light'), 'light');
  assert.equal(themeValue('dark'), 'dark');
  assert.throws(() => themeValue('sepia'), RangeError);
});

test('the menu and usedScheme together: dark wins over a light system only when chosen', () => {
  assert.equal(usedScheme(themeValue('dark'), 'light'), 'dark');
  assert.equal(usedScheme(themeValue('light'), 'dark'), 'light');
  assert.equal(usedScheme(themeValue('system'), 'dark'), 'dark');
});

test('forced colours remove shadows and gradients', () => {
  assert.equal(forcedComputed('box-shadow', '0 0 0 4px #f26b1d'), 'none');
  assert.equal(forcedComputed('text-shadow', '1px 1px 0 red'), 'none');
  assert.equal(forcedComputed('background-image', 'linear-gradient(red, blue)'), 'none');
  assert.equal(forcedComputed('background-image', 'url("photo.jpg"), linear-gradient(red, blue)'), 'url("photo.jpg"), linear-gradient(red, blue)');
});

test('forced colours reset color-scheme, accent-color and scrollbar-color, and keep the rest', () => {
  assert.equal(forcedComputed('color-scheme', 'dark'), 'light dark');
  assert.equal(forcedComputed('accent-color', '#0044cc'), 'auto');
  assert.equal(forcedComputed('scrollbar-color', 'red blue'), 'auto');
  assert.equal(forcedComputed('outline-color', 'transparent'), 'transparent');
  assert.equal(forcedComputed('border-radius', '6px'), '6px');
});
A hint

For usedScheme, first choose the text to read: pageSchemes when value is 'normal' (and an empty string when there is no meta element), otherwise value. Split it on spaces and keep only the words light and dark; the rest is three ifs. forcedComputed is a switch on the property, with /url\(/i.test(value) for background-image.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A card uses background-color: light-dark(#ffffff, #1c1f26) and nothing on the page sets color-scheme. The visitor's system is set to dark. What colour is the card?

    Choose one answer.

    Show the answer to question 1

    Answer: White, because without color-scheme the element supports no scheme and gets the light default

    light-dark() follows the element's used colour scheme, not the media query. With color-scheme: normal and no meta element, the used scheme is the browser default, light, so all three engines painted card A white even with a dark preference. color-scheme: light dark on the root fixes it.

  2. Question 2 of 6 A theme menu sets color-scheme: dark on the root element while the system stays light. Which rule still shows its light colours?

    Choose one answer.

    Show the answer to question 2

    Answer: A rule inside @media (prefers-color-scheme: dark)

    The media query reports the user's system setting and knows nothing about the page's menu. light-dark(), form controls and system colours follow the used colour scheme, which the menu changed. In the lesson's run the media-query banner stayed light in all three engines while the card and the menu turned dark.

  3. Question 3 of 6 Why does a page add <meta name="color-scheme" content="light dark"> when its CSS already sets color-scheme: light dark on :root?

    Choose one answer.

    Show the answer to question 3

    Answer: So the browser knows before the CSS loads, and can paint the first frame dark instead of flashing white

    The HTML Standard defines the meta element so the browser can render the page background with the right scheme immediately, rather than waiting for all the CSS. Without it, a dark-mode visitor may see a white page for a moment on a slow connection.

  4. Question 4 of 6 With forced colours mode on, which of these are removed or reset? Choose every one.

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • A background-image made of a gradient
    • A focus ring drawn only with box-shadow

    CSS Color Adjustment Level 1 computes box-shadow and text-shadow to none, and background-image to none unless it contains a url() image. A transparent outline is kept and given a visible system colour, which is why it makes a good fallback ring; in Chromium's and Firefox's emulation the lesson's button kept its outline while the orange box-shadow ring disappeared.

  5. Question 5 of 6 Two status chips differ only in background colour, green for online and red for offline. What happens to them in forced colours mode?

    Choose one answer.

    Show the answer to question 5

    Answer: Both get the same system background, so they look identical unless something besides colour differs

    Forced colours mode replaces every author colour with the user's palette, so in Chromium's and Firefox's emulation both chips got a white background and black text. The text labels, the icons and the dashed border of the offline chip are what still tell them apart, which is what WCAG's success criterion 1.4.1 asks for anyway.

  6. Question 6 of 6 In the lesson's run, what did the system colour Field compute to under color-scheme: dark?

    Choose one answer.

    Show the answer to question 6

    Answer: A different value in each engine: rgb(59, 59, 59) in Chromium, rgb(43, 42, 51) in Firefox, a translucent white in WebKit

    System colours belong to the browser and the operating system, so every engine chooses its own dark palette: the dark Canvas was rgb(18, 18, 18), rgb(28, 27, 34) and rgb(30, 30, 30). Use them where you want to match the browser, and set your own colours where a design needs exact values.

UI Color Theme Generator (shadcn, Tailwind, Material 3) Generate light and dark themes from a brand colour, with every text pair checked against WCAG AA.

Interview questions

Fresher · What does color-scheme: light dark on the root element do?

It tells the browser that the page is designed for both a light and a dark colour scheme, so the browser may use the dark one when the user prefers it. The browser then draws everything it controls in that scheme: the page background, form controls, scrollbars and the system colours such as Canvas and Field. It also decides which value light-dark() picks, so light-dark(white, black) turns black only where the used scheme is dark. It does not change any colour the page sets itself with a plain value. Without it, the used scheme stays light even when the user prefers dark, which is why light-dark() seems not to work. A <meta name="color-scheme" content="light dark"> element says the same before the CSS loads, which avoids a white flash.

Key takeaways

  • Put color-scheme: light dark on :root and the matching meta element in the HTML: form controls, scrollbars, the canvas and system colours then follow the reader’s scheme.
  • light-dark() follows the used colour scheme, not prefers-color-scheme: without color-scheme it always picks its light value.
  • A theme menu needs one change, color-scheme on the root; rules written inside prefers-color-scheme media queries ignore it.
  • Check every colour pair in both schemes; a dark theme needs its own lighter accents.
  • In forced colours mode shadows and gradients vanish and colours are replaced: carry meaning in text, icons and borders, and draw focus with an outline.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.