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
: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
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.