Dark Mode and Modern Colour Functions in CSS

Add dark mode with prefers-color-scheme, color-scheme and light-dark(), build a theme toggle, and mix colours with color-mix(), oklch() and relative colours.

Lesson 43 of 49Intermediate16 min read

Most phones and computers now have a dark mode setting, and many people use it all the time: at night, to save battery on OLED screens, or because bright white pages strain their eyes. A website that ignores this setting flashes a white page at someone who has asked their whole device to be dark. Supporting dark mode is now an expected part of a polished site.

CSS makes it straightforward. The prefers-color-scheme media query tells you which mode the visitor's device uses. The color-scheme property tells the browser to darken its own built-in parts, such as scrollbars and form controls. And the newer light-dark() function lets you write both colours in a single declaration.

The secret to doing it cleanly is CSS variables: define every colour once as a variable, then swap the values for dark mode. In this lesson you'll build a theme that follows the device setting, add a manual toggle, and use modern colour functions such as color-mix() and oklch() to create consistent palettes with less effort.

Following the device setting

CSS
:root {
  --bg: #ffffff;
  --text: #1f2937;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #e2e8f0;
  }
}

body { background: var(--bg); color: var(--text); }
  • prefers-color-scheme is light or dark, depending on the visitor's system setting.
  • Only the variable values change; every rule that uses them updates automatically.
  • Name variables by purpose (--bg, --text, --border), not by colour (--white), so the names still make sense in dark mode.

color-scheme and light-dark()

CSS
:root { color-scheme: light dark; }

body {
  background: light-dark(#ffffff, #0f172a);
  color: light-dark(#1f2937, #e2e8f0);
}
  • color-scheme: light dark says the page supports both. The browser then uses dark scrollbars, form controls and default colours in dark mode.
  • light-dark(A, B) returns A in light mode and B in dark mode. It needs color-scheme to be set.
  • Setting color-scheme: dark on one element forces dark mode just for that part, which is the basis for a manual theme switch.

Adding a theme toggle

Many sites offer a button to override the device setting. A simple approach sets an attribute on the root element:

CSS
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

With light-dark() colours, that's the whole theme switch. JavaScript sets document.documentElement.dataset.theme = 'dark' when the button is pressed, and can save the choice with localStorage so it's remembered next visit.

Modern colour functions

FunctionWhat it doesExample
color-mix()Mixes two colourscolor-mix(in oklch, var(--brand) 20%, white) for a light tint
oklch()Lightness, chroma (colourfulness) and hueoklch(65% 0.18 150)
Relative coloursAdjust parts of an existing colouroklch(from var(--brand) calc(l - 0.1) c h) for a darker shade
rgb(r g b / a)Space-separated syntax with alphargb(0 0 0 / 0.5)

oklch() is designed around how people perceive colour, so two colours with the same lightness value look equally light. That makes it much easier to build tints, shades and accessible dark palettes than with hex codes.

CSS Dark Mode Syntax

Dark mode toolkit

:root { color-scheme: light dark; --brand: oklch(60% 0.17 150); }
:root[data-theme="dark"] { color-scheme: dark; }
body { background: light-dark(#fff, #0f172a); color: light-dark(#111827, #e5e7eb); }
.tint { background: color-mix(in oklch, var(--brand) 15%, transparent); }
@media (prefers-color-scheme: dark) { img { filter: brightness(0.9); } }

CSS Dark Mode Summary

  • @media (prefers-color-scheme: dark) detects the visitor's dark mode setting.
  • Define colours as variables and swap only the values for dark mode.
  • color-scheme: light dark darkens built-in browser parts; light-dark() picks the matching colour.
  • A data-theme attribute and a few lines of JavaScript add a manual toggle.
  • color-mix() and oklch() make consistent tints, shades and palettes.

CSS Dark Mode Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

A theme with variables

Change your device to dark mode, or toggle the forced theme with the button.

.app {
  --bg: #ffffff; --surface: #f1f5f9; --text: #0f172a; --muted: #64748b; --accent: #16a34a;
  background: var(--bg); color: var(--text);
  padding: 16px; border-radius: 14px; font-family: sans-serif;
  transition: background 0.3s, color 0.3s;
}

@media (prefers-color-scheme: dark) {
  .app:not([data-theme="light"]) { --bg: #0f172a; --surface: #1e293b; --text: #e2e8f0; --muted: #94a3b8; --accent: #4ade80; }
}
.app[data-theme="dark"] { --bg: #0f172a; --surface: #1e293b; --text: #e2e8f0; --muted: #94a3b8; --accent: #4ade80; }

.muted { color: var(--muted); }
.panel { background: var(--surface); padding: 12px; border-radius: 10px; margin-bottom: 12px; }
.panel strong { color: var(--accent); }
button { background: var(--accent); color: var(--bg); border: 0; padding: 8px 14px; border-radius: 8px; font-weight: 600; cursor: pointer; }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2The light theme's colours are variables.
  2. Lines 8 to 10Dark values apply when the device is dark, unless the light theme is forced.
  3. Line 11The toggle can force dark mode on any device.
  4. Lines 13 to 16Components only ever use the variables.
Example 2

light-dark() in one line each

.auto {
  color-scheme: light dark;
  background: light-dark(#f8fafc, #111827);
  color: light-dark(#111827, #f3f4f6);
  border: 1px solid light-dark(#e5e7eb, #374151);
  padding: 14px;
  margin-bottom: 10px;
  border-radius: 12px;
  font-family: sans-serif;
}

.forced { color-scheme: dark; }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2The box supports both schemes.
  2. Lines 3 to 5Each colour has a light and a dark value.
  3. Line 12Forcing color-scheme: dark switches every light-dark() value and the input styling.
Example 3

Palettes with color-mix() and oklch()

:root { --brand: oklch(62% 0.19 255); }

.swatches { display: flex; gap: 8px; font: 600 13px sans-serif; }
.s { display: grid; place-items: center; width: 70px; height: 70px; border-radius: 10px; color: white; }

.s1 { background: color-mix(in oklch, var(--brand) 10%, white); color: #1e3a8a; }
.s2 { background: color-mix(in oklch, var(--brand) 30%, white); color: #1e3a8a; }
.s3 { background: var(--brand); }
.s4 { background: oklch(from var(--brand) calc(l - 0.2) c h); }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1One brand colour in oklch: lightness, chroma, hue.
  2. Lines 6 to 7Mixing with white creates light tints for backgrounds.
  3. Line 9A relative colour keeps the hue and chroma but lowers the lightness.

Common CSS Dark Mode Mistakes

Hard-coded colours everywhere

Don't
.card { background: white; color: #111; }
@media (prefers-color-scheme: dark) { .card { background: #111; color: white; } }
/* repeated for every component */
Do
:root { --surface: white; --text: #111; }
@media (prefers-color-scheme: dark) { :root { --surface: #111; --text: white; } }
.card { background: var(--surface); color: var(--text); }

Forgetting color-scheme

Don't
body { background: #0f172a; color: white; } /* inputs and scrollbars stay bright white */
Do
:root { color-scheme: light dark; }

Pure black and white

Don't
@media (prefers-color-scheme: dark) { body { background: #000; color: #fff; } }
Do
@media (prefers-color-scheme: dark) { body { background: #0f172a; color: #e2e8f0; } }

Maximum contrast causes glare and halo effects for many readers.

CSS Dark Mode in Real Projects

Dark mode in practice

GitHub, YouTube, X and most operating systems offer light, dark and "match system" options, implemented with exactly this pattern: variables, prefers-color-scheme, and an attribute for the manual choice. Design systems such as Tailwind define colour palettes in oklch(). Codevra's own pages switch theme with CSS variables, so every lesson and code block follows your setting.

CSS Dark Mode Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Apply these styles only when the device is in dark mode.

@media (: dark) {
  :root { --bg: #0f172a; }
}
Multiple choiceeasy

What does color-scheme: light dark tell the browser?

Multiple choicemedium

What does light-dark(white, black) return in dark mode?

Multiple choicemedium

Why name variables --text and --bg rather than --black and --white?

Fix the bugmedium

The panel should be dark when data-theme="dark" is set on it, using variables. Fix the theme rule and the variable usage.

.panel { --bg: rgb(255, 255, 255); --text: rgb(17, 24, 39); background: white; color: var(--text); }
.panel.dark { --bg: rgb(15, 23, 42); --text: rgb(226, 232, 240); }
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Dark background via the variable
  • Light text
  • Uses the data-theme attribute

CSS Dark Mode Quiz

5 questions to check what you learned.

  1. Which media query detects dark mode?

    • @media (dark-mode: on)
    • @media (theme: dark)
    • @media (prefers-color-scheme: dark)
    • @media screen and (dark)
  2. How does a manual toggle usually work?

    • It reloads a dark stylesheet
    • It changes the operating system setting
    • It inverts the page with a filter
    • JavaScript sets an attribute such as data-theme on the root, and CSS responds
  3. What does color-mix(in oklch, blue 20%, white) produce?

    • A light blue tint
    • Pure blue
    • Pure white
    • An error
  4. Why is oklch() useful for palettes?

    • It's shorter than hex
    • Equal lightness values look equally light to people
    • It's the only colour format in dark mode
    • It supports transparency
  5. Why shouldn't dark mode use pure black with pure white text?

    • Browsers don't support it
    • The extreme contrast causes glare and eye strain for many readers
    • It's slower
    • It fails contrast checks

CSS Dark Mode Challenge

A themeable page

  1. On :root, set color-scheme: light dark
  2. Make .page use background: light-dark(rgb(255, 255, 255), rgb(15, 23, 42)) and color: light-dark(rgb(17, 24, 39), rgb(226, 232, 240))
  3. Make .page[data-theme="dark"] force color-scheme: dark
  4. Give .tag a background of color-mix(in srgb, rgb(22, 163, 74) 20%, transparent)
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Root supports both schemes
  • Forced dark page is dark
  • Uses light-dark()
  • Tag uses color-mix