Dark Mode with CSS Variables
Implementing dark mode without a heavy theming library comes down to a stable token contract and a media query. This snippet covers the system-preference dark mode using `prefers-color-scheme`, a class-based override that lets users opt in or out, and a per-component theme that nests tokens for a single section.
580 views
9
:root {
--bg: white;
--fg: hsl(220 15% 15%);
--accent: hsl(220 90% 50%);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(220 20% 10%);
--fg: hsl(220 15% 92%);
--accent: hsl(220 90% 65%);
}
}
body {
background: var(--bg);
color: var(--fg);
}Defining tokens (--bg, --fg, --accent) at :root gives every consumer a single source of truth. The prefers-color-scheme: dark media query overrides the same variables when the OS prefers dark, which automatically themes every element that references them. This is the lowest-overhead path to a respectful dark mode, since it follows the user's existing preference without any UI. The accent stays the same hue but lightens for dark mode so it keeps passing contrast on the new background.
:root {
--bg: white;
--fg: hsl(220 15% 15%);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(220 20% 10%);
--fg: hsl(220 15% 92%);
}
}
html.theme-light {
--bg: white;
--fg: hsl(220 15% 15%);
}
html.theme-dark {
--bg: hsl(220 20% 10%);
--fg: hsl(220 15% 92%);
}Many users want to override the system preference (read in dark mode at noon, light mode at night). Adding theme-light and theme-dark classes on <html> lets a small JS toggle pin the theme, while the media query continues to drive the default for users who never toggle. The class rules sit AFTER the media query so they win the cascade when applied. The toggle itself is one line of JS: document.documentElement.classList.toggle('theme-dark').
.card {
--card-bg: white;
--card-fg: hsl(220 15% 15%);
background: var(--card-bg);
color: var(--card-fg);
padding: 1rem;
border-radius: 0.5rem;
}
.card.dark {
--card-bg: hsl(220 20% 10%);
--card-fg: hsl(220 15% 92%);
}
.card.brand {
--card-bg: hsl(220 90% 50%);
--card-fg: white;
}Local custom properties scoped to a component let you re-skin one card without rewriting any rules. The card consumes its own --card-bg and --card-fg, and adding a class (.dark, .brand) flips just those tokens. Because CSS variables inherit, nested components inside the card can read the same tokens to stay in sync. This is how design systems expose 'inverse' variants (dark cards on a light page, branded sections inside neutral pages) without proliferating one-off styles.
