CSS Color-Scheme Table

PieceWhat it doesField note
color-scheme: light darkOpt the page into bothTells the UA THIS page handles dark - enables UA dark chrome
light-dark(lightVal, darkVal)One property, two valuesThe 2024 function - color flips with the active scheme
meta name=color-schemeDeclare before CSS loadsKills the white-flash on dark-first loads - head metadata
prefers-color-schemeThe media queryFull-stylesheet switch - the matchMedia counterpart reads it
forced-colors: activeThe high-contrast modeUser FORCED palette - respect system colors, do not fight
color-scheme on root onlyInherit downSet on :root; form controls and scrollbars inherit it
light-dark + custom propsToken pair pattern--bg: light-dark(white, #111) - one token, both schemes
Only THREE system colorsThe honest floorCanvas, CanvasText, LinkText - forced-colors minimum set
Reference: the MDN color-scheme reference. Dark mode has three tiers and most sites need only the first two: color-scheme: light dark on :root opts into UA-rendered dark chrome (form controls, scrollbars), light-dark() flips individual COLOR values with the active scheme, and prefers-color-scheme is the full-stylesheet media query for structural changes. Bottom line: declare color-scheme in a META tag before CSS loads (kills the white-flash), build tokens as --bg: light-dark(white, #111) pairs, and forced-colors mode is the user's palette - respect Canvas/CanvasText, never fight it. Related tools: matchMedia table (the JS detection side), custom properties table (the token system light-dark feeds), and meta tags table (the head declaration).

Dark mode has three tiers, and most sites need only the first two: color-scheme: light dark on :root opts the page into UA-rendered dark chrome (form controls, scrollbars, backgrounds), and light-dark() flips individual color values with whichever scheme is active - the 2024 function that ends dual-token-bookkeeping.

Bottom line: declare color-scheme in a META tag in the head BEFORE the stylesheet loads - it is the white-flash kill on dark-first loads (the UA paints the canvas dark before your CSS arrives). Build tokens as pairs (--bg: light-dark(white, #111)) and the whole theme is one line per token.

The honest part: forced-colors: active (Windows High Contrast) is the user OVERRIDING your palette entirely - the correct response is mapping to system colors (Canvas, CanvasText, LinkText), not fighting with your dark theme. Three system colors are the honest accessible floor.

How to use

  1. Opt in at the root: :root { color-scheme: light dark; } - form controls, scrollbars and the canvas follow the OS setting with zero other CSS.
  2. Token pairs everywhere: --surface: light-dark(#fff, #1a1a1a); background: var(--surface) - one declaration carries both schemes; themes become one-line edits.
  3. Kill the flash: <meta name=color-scheme content="light dark"> in head before stylesheets - the browser paints the correct canvas before your CSS arrives.

Frequently asked questions

What is the difference between color-scheme, light-dark() and prefers-color-scheme?

Three tiers of the same switch. color-scheme is a DECLARATION: 'this page supports both schemes' - it makes the UA render its own pieces (form controls, scrollbars, default canvas) to match the active scheme. light-dark() is a VALUE function: it resolves to its first argument in light and second in dark - per-property theming without media queries. prefers-color-scheme is the MEDIA QUERY: the full-stylesheet switch for changes beyond colors (layout, images, shadows - dark UIs often want softer shadows and different imagery). Tier one for free UA chrome, tier two for colors, tier three for structure.

Why does my dark page flash white before loading?

The canvas paints before your CSS does. The browser starts rendering with its default scheme (light on most systems) the moment HTML begins arriving; when your stylesheet finally declares the dark theme, there is a visible white flash - on slow connections, seconds of it. The fix lives in the head: <meta name=color-scheme content="light dark"> tells the UA the page handles dark BEFORE any stylesheet, so the canvas and built-in UI paint dark immediately. The complementary piece for user-preference-wins themes: a tiny inline script that sets the resolved theme on <html> before the stylesheet - same principle, user override edition.

How does light-dark() change the token architecture?

It halves the token bookkeeping. The old pattern: two token sets plus a [data-theme] attribute swap, or media-query overrides for every token - the palette defined twice and kept in sync by discipline. The new pattern: ONE custom property carries BOTH values - --surface: light-dark(#ffffff, #1a1a1a) - and the active scheme picks the branch automatically. Overriding per theme becomes per-element too: color-scheme can be set on a SUBTREE (a permanently-dark sidebar inside a light page), and its light-dark() values flip locally. The honest limit: light-dark() only works where color-scheme is actually declared, and it resolves values only - it cannot restyle shadows, borders-as-structure or images.

What does forced-colors mode ask of my dark theme?

Surrender - politely. forced-colors (Windows High Contrast) replaces the page's palette with the USER's chosen system colors; your carefully tuned dark palette is irrelevant by design, and that is the accessibility feature working. The correct response: use system color keywords (Canvas for backgrounds, CanvasText for text, LinkText for links) as your forced-colors baseline so components stay legible, keep borders and underlines for non-color affordances (system colors can make subtle cues vanish), and test with the mode on. The one honest floor: Canvas, CanvasText and LinkText - if a component is unreadable with just those three, the component relies on color alone and fails before any theme does.

Related tools