Web design
Dark mode in 2026: één stylesheet, twee thema's, zero drift
Het is 23:00 en je donkere stylesheet loopt weer één commit achter op de lichte. In 2026 kan dat anders, en de oplossing past in één CSS-bestand.

Het is 23:00 en de donkere stylesheet loopt weer één commit achter op de lichte. Iemand heeft vorige week een nieuwe kaart-achtergrond toegevoegd, alleen in style.css, en nu heeft de dark build een witte rechthoek die niemand opmerkte tot er een screenshot in de client-Slack belandde. We kennen het. De meeste teams leveren dark mode nog steeds als een tweede laag aan: een parallel bestand, een gedupliceerde set variabelen, een ontwerper die moet onthouden om beide helften bij te werken zodra een kleur twee procent verschuift. In 2026 is er geen reden om dat nog te doen.
Dit is een korte, uitgesproken rondleiding door hoe we dit jaar dark mode opzetten op nieuwe ABN-sites. Eén bestand. Eén bron van waarheid. De browser doet de switch. Hieronder de stappen in de volgorde waarin we ze uitvoeren.
Stap 1: stop met denken in thema's, begin met denken in tokens
De eerste stap is geen CSS-wijziging. Het is een woordenschat-wijziging. Stop met dingen --white en --black noemen. Schrijf in geen enkel componentbestand nog background: #fff. Elke kleur in je interface hoort via een semantisch token op te lossen: --surface, --surface-raised, --text, --text-muted, --border, --accent. Pakt een component een ruwe hex-waarde, dan heeft het je themasysteem al omzeild, en je vindt het later terug als dark-mode bug.
Het idee achter de token-laag is dat componenten geen mening hebben over de mode waarin ze staan. Ze vragen om de gedempte tekstkleur, niet om #666. Of dat uitkomt op bijna-zwart of zacht grijs is het probleem van het thema, niet van de kaart.
Stap 2: bind de tokens met light-dark()
Dit deel bestond een paar jaar geleden nog niet. De CSS-functie light-dark() neemt twee kleurargumenten en kiest de juiste op basis van de actieve color-scheme. De functie zit sinds halverwege 2024 in elke grote engine en is op elke moderne site veilig te gebruiken zonder fallback. MDN documenteert de functie en Can I Use toont de support matrix.
De hele token-laag valt samen in één blok:
:root {
color-scheme: light dark;
--surface: light-dark(#fafaf9, #0f1115);
--surface-raised: light-dark(#ffffff, #171a20);
--text: light-dark(#1a1a1a, #ececec);
--text-muted: light-dark(#5a5a5a, #9aa0aa);
--border: light-dark(#e6e6e3, #2a2d33);
--accent: light-dark(#0a5cff, #6aa3ff);
--accent-ink: light-dark(#ffffff, #0b1020);
}
body {
background: var(--surface);
color: var(--text);
}
Dat is het hele thema. Geen @media (prefers-color-scheme: dark)-blok. Geen gedupliceerde selectors. Elke afnemer van var(--text) verderop krijgt automatisch de juiste kleur, omdat light-dark() opnieuw wordt geëvalueerd zodra de effectieve color-scheme op dat element verandert.
Stap 3: declareer color-scheme zodat de browser meewerkt
De regel color-scheme: light dark hierboven doet twee dingen. Ten eerste activeert hij de functie light-dark(). Daarnaast vertelt hij de browser dat je pagina in beide modes wil renderen. Dat zet de native dark scrollbars, formuliercontrols en de donkere achtergrondflash tijdens navigatie aan. Zonder die regel ziet een gebruiker met donker OS bij eerste paint nog steeds een witte flits, en komen je <input>-elementen er als felle witte rechthoeken uit, zelfs als de rest van de pagina donker is.
Als je één regel uit dit hele stuk vergeet, vergeet deze dan niet. Het is de goedkoopste toegankelijkheidswinst op de hele pagina.
Stap 4: schaduwen en randen zijn geen kleuren
De grootste fout die we in junior dark-mode werk zien, is schaduwen als kleuren behandelen. Een schaduw die op een wit oppervlak leest als een zachte grijze blur, wordt onzichtbaar op bijna-zwart. De oplossing is niet om de schaduw lichter te maken. Schaduwen in dark mode horen donkerder te zijn, niet lichter, en ze leunen op een gekoppelde highlight (een 1px inner top border met lage opacity) om hoogte te suggereren.
:root {
--shadow-1: light-dark(
0 1px 2px rgb(0 0 0 / 0.06),
0 1px 2px rgb(0 0 0 / 0.55)
);
--shadow-2: light-dark(
0 8px 24px rgb(0 0 0 / 0.08),
0 8px 24px rgb(0 0 0 / 0.6)
);
--hairline: light-dark(
inset 0 0 0 1px rgb(0 0 0 / 0.04),
inset 0 1px 0 rgb(255 255 255 / 0.05)
);
}
.card {
background: var(--surface-raised);
box-shadow: var(--shadow-1), var(--hairline);
border: 1px solid var(--border);
}
Ogen je dark-mode kaarten plat, dan is het antwoord bijna nooit een lichtere achtergrond. Het is een diepere schaduw plus een vage highlight op de bovenrand.
Stap 5: de onderdelen die geen kleur zijn
Een paar vlakken weigeren mee te bewegen. Inline SVG-iconen die fill="#000" hardcoded hebben, blijven zwart op een zwarte achtergrond. Screenshots op marketingpagina's houden hun witte chrome. Codeblokken met syntax highlighting hebben vaak hun eigen ingebakken palet.
Drie regels die ons later uren besparen:
- SVG's in de UI gebruiken
fill="currentColor"en erven het tekst-token. Geen iconen meer die in dark mode verdwijnen. - Product-screenshots krijgen een
<picture>met eensource media="(prefers-color-scheme: dark)"-variant wanneer de screenshot zelf de content is. Inverteer het beeld niet met een CSS-filter. Dat verkleurt foto's blauw. - Codeblokken laden via dezelfde media query één van twee highlight-stylesheets, of gebruiken een thema dat door tokens gestuurd wordt. Prism en Shiki ondersteunen nu allebei custom CSS-variabelen.
Stap 6: geef gebruikers een handmatige override
De systeemvoorkeur is de juiste default, maar elk product heeft een in-page toggle nodig voor de gebruiker wiens OS bij zonsondergang automatisch omschakelt en die nu om 17:00 met samengeknepen ogen naar een witte factuur zit te kijken. De truc is om de toggle één attribuut op <html> te laten wijzigen en color-scheme de rest te laten doen.
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
const root = document.documentElement;
const saved = localStorage.getItem('theme');
if (saved) root.dataset.theme = saved;
export function setTheme(next) {
if (next === 'system') {
delete root.dataset.theme;
localStorage.removeItem('theme');
} else {
root.dataset.theme = next;
localStorage.setItem('theme', next);
}
}
Omdat elke kleur via light-dark() wordt opgelost, en light-dark() de effectieve color-scheme op het element leest, herschildert het wisselen van dat ene attribuut de hele site. Geen class cascade. Geen flits. Geen JavaScript dat in losse componenten grijpt.
Controleer je contrast in beide modes, niet alleen in één
Dark mode is waar contrast-bugs zich verstoppen. Een grijs dat slaagt op WCAG AA tegen wit, slaagt bijna nooit tegen bijna-zwart, omdat dezelfde hex-code een andere contrastratio heeft tegen een andere achtergrond. Draai je contrastchecker ook tegen het donkere oppervlak. Bodytekst heeft 4.5:1 nodig, grote tekst 3:1, en je muted text-token is degene die als eerste faalt.
Toen we dit voorjaar het donkere thema bouwden voor het facturatie-dashboard van een klant, slaagde het muted token (#888) op wit en faalde het stilletjes op het donkere oppervlak met 3.9:1. We pakten het op door elk token-paar door een scriptje te halen dat beide ratios naast elkaar in de terminal afdrukt. Dat scriptje zit nu in onze starter. Het is het nuttigste stukje thema-tooling dat we hebben. Wil je hulp bij het inrichten van dit soort design-system automatisering in je eigen product, dan is dat het werk dat we doen.
Open je site in light mode. Zet het OS op donker. Alles wat wit oplicht, een ontbrekende rand heeft of een zwarte SVG op een zwarte kaart toont, staat morgen op de punch list. Dat is de audit van vijf minuten.
Kern
Bind elke kleur via light-dark(), declareer color-scheme op :root, en één stylesheet bedient beide thema's zonder drift.
FAQ
Heb ik de prefers-color-scheme media query nog nodig?
Alleen voor zaken die geen CSS zijn, zoals een picture-element dat tussen twee verschillende screenshots wisselt. Voor kleuren vervangen light-dark() plus color-scheme de media query volledig.
Wat als een gebruiker een oudere browser heeft zonder ondersteuning voor light-dark()?
Elke grote engine ondersteunt het sinds 2024. Voor heel oude browsers zet je een enkele fallback-kleur op dezelfde custom property vóór de light-dark()-aanroep, dan krijgen ze een bruikbaar light-thema.
Moet dark mode gewoon elke kleur omkeren?
Nee. Inversie verkleurt foto's blauw, sloopt brand-kleuren en levert flets uitziende grijstinten op. Kies elk donker token met de hand, en check daarna het contrast tegen het echte donkere oppervlak.
Waar hoort de thema-toggle in de UI?
In account of instellingen, niet in de hoofdnavigatie. De meeste gebruikers raken hem nooit aan. Een vaste toggle in de header straalt besluiteloosheid uit en pakt een plek af van productnavigatie.