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
:root {
--bg: #ffffff;
--text: #1f2937;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #e2e8f0;
}
}
body { background: var(--bg); color: var(--text); }prefers-color-schemeislightordark, 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()
:root { color-scheme: light dark; }
body {
background: light-dark(#ffffff, #0f172a);
color: light-dark(#1f2937, #e2e8f0);
}color-scheme: light darksays 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 needscolor-schemeto be set.- Setting
color-scheme: darkon 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:
: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
| Function | What it does | Example |
|---|---|---|
color-mix() | Mixes two colours | color-mix(in oklch, var(--brand) 20%, white) for a light tint |
oklch() | Lightness, chroma (colourfulness) and hue | oklch(65% 0.18 150) |
| Relative colours | Adjust parts of an existing colour | oklch(from var(--brand) calc(l - 0.1) c h) for a darker shade |
rgb(r g b / a) | Space-separated syntax with alpha | rgb(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 darkdarkens built-in browser parts;light-dark()picks the matching colour.- A
data-themeattribute and a few lines of JavaScript add a manual toggle. color-mix()andoklch()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.
A theme with variables
Change your device to dark mode, or toggle the forced theme with the button.
<div class="app" id="app">
<h3>Account summary</h3>
<p class="muted">Last updated today at 09:41</p>
<div class="panel">Balance: <strong>£1,240.50</strong></div>
<button onclick="const a = document.getElementById('app'); a.dataset.theme = a.dataset.theme === 'dark' ? 'light' : 'dark'">Toggle theme</button>
</div>.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; }Line-by-line explanation
- Line 2The light theme's colours are variables.
- Lines 8 to 10Dark values apply when the device is dark, unless the light theme is forced.
- Line 11The toggle can force dark mode on any device.
- Lines 13 to 16Components only ever use the variables.
light-dark() in one line each
<div class="auto">
<p>This box follows your device's light or dark setting.</p>
<input placeholder="Built-in controls match too">
</div>
<div class="auto forced">
<p>This box is always dark.</p>
<input placeholder="Dark input">
</div>.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; }Line-by-line explanation
- Line 2The box supports both schemes.
- Lines 3 to 5Each colour has a light and a dark value.
- Line 12Forcing
color-scheme: darkswitches everylight-dark()value and the input styling.
Palettes with color-mix() and oklch()
<div class="swatches">
<span class="s s1">10%</span><span class="s s2">30%</span><span class="s s3">brand</span><span class="s s4">darker</span>
</div>: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); }Line-by-line explanation
- Line 1One brand colour in oklch: lightness, chroma, hue.
- Lines 6 to 7Mixing with white creates light tints for backgrounds.
- Line 9A relative colour keeps the hue and chroma but lowers the lightness.
Common CSS Dark Mode Mistakes
Hard-coded colours everywhere
.card { background: white; color: #111; }
@media (prefers-color-scheme: dark) { .card { background: #111; color: white; } }
/* repeated for every component */: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
body { background: #0f172a; color: white; } /* inputs and scrollbars stay bright white */:root { color-scheme: light dark; }Pure black and white
@media (prefers-color-scheme: dark) { body { background: #000; color: #fff; } }@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.
Apply these styles only when the device is in dark mode.
@media (: dark) {
:root { --bg: #0f172a; }
}
prefers-color-scheme.What does color-scheme: light dark tell the browser?
What does light-dark(white, black) return in dark mode?
Why name variables --text and --bg rather than --black and --white?
The panel should be dark when data-theme="dark" is set on it, using variables. Fix the theme rule and the variable usage.
<div class="panel" data-theme="dark">Settings</div>.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); }- Dark background via the variable
- Light text
- Uses the data-theme attribute
CSS Dark Mode Quiz
5 questions to check what you learned.
Which media query detects dark mode?
- @media (dark-mode: on)
- @media (theme: dark)
- @media (prefers-color-scheme: dark)
- @media screen and (dark)
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
What does color-mix(in oklch, blue 20%, white) produce?
- A light blue tint
- Pure blue
- Pure white
- An error
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
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
- On
:root, setcolor-scheme: light dark - Make
.pageusebackground: light-dark(rgb(255, 255, 255), rgb(15, 23, 42))andcolor: light-dark(rgb(17, 24, 39), rgb(226, 232, 240)) - Make
.page[data-theme="dark"]forcecolor-scheme: dark - Give
.tagabackgroundofcolor-mix(in srgb, rgb(22, 163, 74) 20%, transparent)
<div class="page" data-theme="dark">
<h2>Dashboard</h2>
<span class="tag">New</span>
</div>- Root supports both schemes
- Forced dark page is dark
- Uses light-dark()
- Tag uses color-mix