Een goede CSS dark mode bestaat uit drie onderdelen: CSS-variabelen die je kleuren vanuit één plek beheren, de media query prefers-color-scheme die de systeemvoorkeur van de gebruiker uitleest, en een kleine toggle zodat iedereen het thema handmatig kan wisselen. Koppel deze drie correct en dark mode wordt consistent, blijvend en vrij van de flicker die zoveel sites bij het laden teistert. In deze gids bouw ik het stap voor stap op met echte, werkende code.
Verplaats je kleuren naar CSS-variabelen
Het kernidee van een donker thema is dit: kleurwaarden kunnen verwisselen zonder iets in je HTML aan te raken. De schoonste manier daarvoor is om elke kleur te beheren via CSS custom properties die je op :root declareert. Zo verwijzen je componenten naar abstracte namen en staat de echte kleurwaarde op precies één plek.
:root {
--bg: #ffffff;
--text: #1a1a1a;
--muted: #5a5a5a;
--surface: #f4f4f5;
--accent: #2563eb;
}
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--surface);
border: 1px solid #e4e4e7;
}
Van thema wisselen is nu zo simpel als de waarde van deze vijf variabelen bijwerken. Je hoeft nooit honderden regels zoals .card of body stuk voor stuk met de hand aan te passen.
Lees de systeemvoorkeur met prefers-color-scheme
De meeste gebruikers hebben al een thema gekozen op het niveau van het besturingssysteem. De media query prefers-color-scheme vangt precies die voorkeur op. Je pagina opent dan in lijn met hun apparaatinstelling, zonder dat ze ergens op klikken:
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f10;
--text: #ededed;
--muted: #a1a1aa;
--surface: #1a1a1d;
--accent: #60a5fa;
}
}
Let op een paar dingen. Gebruik in dark mode een heel donker grijs (zoals #0f0f10) in plaats van puur zwart (#000); witte tekst op puur zwart vermoeit de ogen en geeft een waas die "halation" heet. Houd je tekst ook lichtelijk gebroken wit in plaats van puur wit. Verder wil je accentkleuren tegen een donkere achtergrond wat oplichten; een diep blauw dat prachtig oogt in light mode valt in het donker plat.
Voeg een handmatige toggle toe
De systeemvoorkeur is een goed begin, maar gebruikers willen het thema altijd zelf kunnen kiezen. Daarvoor schrijven we de voorkeur als een data-theme-attribuut op het html-element en bouwen we de CSS daaromheen. Structureer de CSS eerst voor drie toestanden: de standaard (licht), het door het systeem aangestuurde donker en het handmatig gekozen donker.
/* Wanneer de gebruiker handmatig donker koos */
:root[data-theme="dark"] {
--bg: #0f0f10;
--text: #ededed;
--muted: #a1a1aa;
--surface: #1a1a1d;
--accent: #60a5fa;
}
/* Wanneer de gebruiker handmatig licht koos, ook al staat het systeem op donker */
:root[data-theme="light"] {
--bg: #ffffff;
--text: #1a1a1a;
}
De subtiliteit hier: als data-theme ontbreekt, blijft prefers-color-scheme de baas; zodra de gebruiker een keuze heeft gemaakt, overschrijft die keuze het systeem. Aan de JavaScript-kant draait het puur om dit attribuut omzetten en de keuze onthouden:
const btn = document.querySelector("#theme-toggle");
btn.addEventListener("click", () => {
const root = document.documentElement;
const current = root.getAttribute("data-theme");
// Bepaal het momenteel zichtbare thema
const isDark = current
? current === "dark"
: matchMedia("(prefers-color-scheme: dark)").matches;
const next = isDark ? "light" : "dark";
root.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
});
Voorkom de flicker (FOUC)
Dit is veruit de meest gemaakte fout: de themavoorkeur pas toepassen nadat de pagina volledig is geladen. In dat geval ziet een gebruiker die dark mode verkiest een paar milliseconden lang een wit scherm — dit heet FOUC (Flash of Unstyled Content), oftewel theme-flicker. De oplossing is om de voorkeur uit localStorage zo vroeg mogelijk toe te passen, in de <head> van de pagina, voordat er ook maar één CSS-regel wordt getekend.
<head>
<script>
(function () {
const saved = localStorage.getItem("theme");
if (saved) {
document.documentElement.setAttribute("data-theme", saved);
}
})();
</script>
<link rel="stylesheet" href="styles.css">
</head>
Omdat dit kleine inline-script vóór het renderen draait, wordt het scherm nooit met de verkeerde kleur getekend. Plaats het script altijd vóór de stylesheet en direct in de HTML; verplaats je het naar een extern bestand, dan brengt netwerklatentie de flicker meteen terug.
Maak de overgangen vloeiend — maar slim
Een zachte kleurovergang bij het wisselen van thema oogt mooi, maar voorzichtigheid is geboden. Pas je transition op alles toe, dan zie je mogelijk een ongewenste animatie bij de allereerste paginalading. Een praktische aanpak is om de overgang alleen op het moment van togglen in te schakelen: JavaScript voegt een tijdelijke class toe en verwijdert die na een dubbele requestAnimationFrame.
:root.theme-transition,
:root.theme-transition * {
transition: background-color .2s ease, color .2s ease;
}
Vergeet voor toegankelijkheid de voorkeur prefers-reduced-motion niet; schakel overgangen uit voor gebruikers die minder beweging willen. Voeg ook een aria-label toe aan de toggle-knop, zodat schermlezers kunnen uitleggen wat hij doet.
Testen en bijschaven
- Contrast: houd in dark mode het tekst/achtergrond-contrast op WCAG AA-niveau (minstens 4,5:1). De contrasttool in de browser-DevTools toont dit direct.
- Afbeeldingen: transparante PNG-logo's kunnen op een donkere achtergrond verdwijnen; serveer indien nodig een aparte variant voor dark mode.
- Schaduwen: box-shadows zijn in dark mode bijna onzichtbaar. Geef diepte met iets lichtere oppervlaktekleuren in plaats van schaduwen.
- theme-color: stem op mobiel de kleur van de browserbalk af op het thema met
<meta name="theme-color">.
Veelgestelde vragen
Werkt prefers-color-scheme in alle browsers?
Ja — elke huidige grote browser (Chrome, Firefox, Safari, Edge) ondersteunt het al jaren. In een heel oude browser wordt de media query simpelweg genegeerd en ziet de gebruiker het standaardthema (licht); het werkt dus veilig als progressive enhancement.
Waarom de voorkeur in localStorage opslaan in plaats van een cookie?
Het thema is een puur client-side visuele voorkeur; het hoeft niet naar de server gestuurd te worden. localStorage wordt synchroon gelezen, zodat het inline-script in je <head> het meteen kan ophalen en flicker voorkomt. Wil je een server-side gerenderd thema, dan is een cookie logischer, omdat die aan het verzoek wordt gehecht.
Is prefers-color-scheme niet genoeg — is een toggle echt nodig?
Strikt genomen niet, maar het wordt aanbevolen. Sommige gebruikers zetten hun apparaat op donker maar willen bepaalde sites in licht lezen (of andersom). Een handmatige toggle geeft hun die controle en verhoogt de tevredenheid.
Een schoon, flicker-vrij donker thema ziet er strak uit én ontziet de ogen van je gebruikers. Wil je dark mode op je site opzetten, een bestaand kleursysteem naar variabelen migreren of je thema-overgangen herstellen, neem contact op — dan zetten we het samen snel live.