Custom properties & prefers-color-scheme

Light and dark themes

Lesson 14 of 20 · 12 min · Module 4: Images, themes and motion

Many people use dark mode, especially in the evening. With colours stored in custom properties, a theme becomes a small set of values to swap, instead of a second copy of every rule.

Start this lesson →

Free. No installation: you write and run the code in your browser, and it is checked automatically.

By the end, you’ll be able to…

  • Store a theme in custom properties
  • Switch values with prefers-color-scheme
  • Force a theme with a data attribute

The idea

Define colours once on :root, use them with var(--bg), and change only the values inside @media (prefers-color-scheme: dark). color-scheme: light dark tells the browser to adapt form controls and scrollbars too. An attribute such as [data-theme="dark"] can force a theme for one section, or for the whole page with a toggle.

:root { --bg: #ffffff; --text: #1f2a24; }

@media (prefers-color-scheme: dark) {
  :root { --bg: #141a16; --text: #e6efe4; }
}

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

01Change values, not rules

Every rule keeps using var(--surface). Only the definition of --surface changes between themes, so a new component automatically supports both.

02Respect the system setting

prefers-color-scheme: dark matches when the operating system or browser is set to dark mode. color-scheme: light dark lets the browser draw matching scrollbars, inputs and checkboxes.

03A switch for people who want to choose

An attribute selector can force a theme regardless of the system setting. A toggle button only needs to set data-theme on the html element, and remember the choice.

<html data-theme="dark">  <!-- set by a toggle -->

[data-theme="dark"] {
  --bg: #141a16;
  --text: #e6efe4;
}

Your turn

1) On :root, define --bg: #ffffff; --text: #1f2a24; --surface: #f4f7ef; --accent: #2f6f4e; and color-scheme: light dark;. 2) Use them: body gets background: var(--bg) and color: var(--text), .card gets background: var(--surface), and .button gets background: var(--accent). 3) Inside @media (prefers-color-scheme: dark), give :root the dark values --bg: #141a16; --text: #e6efe4; --surface: #1f2822; --accent: #8fd18f;. 4) Give [data-theme="dark"] the same dark values plus background: var(--bg); color: var(--text);, so the preview section is always dark.

What the checks look for

  • Light colours are defined on :root
  • The page declares support for both schemes
  • Dark colours apply when people prefer dark
  • The dark surface colour is set too
  • The page background uses the variable
  • Cards use the surface variable
  • The forced dark section has a dark card
  • The forced dark section uses light text
Try it in the editor →

Take this with you

A theme is a set of values; components only refer to them.

Go a little further

Add a high-contrast variant with @media (prefers-contrast: more) that darkens the text and the borders.

Learn HTML & CSS Intermediate for free

20 short lessons with real code and instant checks.

See the whole course →