Web design
OKLCH dark mode voor een nachtdienst-dashboard: een case study
Een Amsterdamse SaaS-studio van zes man vroeg ons het verblindingsprobleem op te lossen. Drie van de vier enterprise-gebruikers draaiden nachtdienst, maar het dashboard was nog altijd geverfd voor een kantoor om twaalf uur 's middags.

Het eerste uur van de opdracht was een screen-share. Drie engineers, drie monitoren, alle dashboard-tabs van hun warehouse-ops SaaS open. Eén engineer had zijn schermhelderheid op twaalf procent gezet. De volgende had een strookje bruin papier over het bovenste kwart van zijn scherm geplakt, precies waar de app-bar zat. De derde had een Chrome-extensie geïnstalleerd die de kleuren paginabreed inverteerde, met als bijwerking dat ook de productfoto's van de klant ondersteboven kwamen te staan. Het was 2 uur 's nachts in Frankfurt en 8 uur 's ochtends in Bangkok en ze zaten hoe dan ook in dienst. Het product had een dark mode. Niemand gebruikte 'm.
De Amsterdamse studio die de tool bouwt — zes mensen, drie daarvan designers, klanten verspreid over de Noord-Europese logistiek — had in februari een klantenonderzoek gedaan. Drie van de vier enterprise-seats gaven aan regelmatig in nachtdienst te werken. Het dashboard had technisch gezien een donker thema. Iemand had het er in 2023 aangeplakt door de body-achtergrond op #1a1a1a te zetten en erop te vertrouwen dat prefers-color-scheme de rest zou afhandelen. Dat deed het niet. Grafieken behielden hun day-mode palet. Agenda-uitnodigingen verblindden operators om 4 uur 's nachts. Het donkere thema was een Frankenstein en klanten zeiden dat ook op elk kwartaaloverleg.
We hebben het hele kleursysteem in acht weken opnieuw gebouwd, van token-sheet tot laatste knop. Hieronder wat we hebben opgeleverd, wat we hebben gemeten tijdens een cohort van twaalf weken na livegang, en wat we anders zouden doen als we het project opnieuw zouden draaien.
OKLCH boven een hex-palet
We hadden ook een hex-palet in een Figma-bestand kunnen opleveren en in twee weken klaar kunnen zijn. Dat hebben we niet gedaan, om twee redenen.
De eerste reden was perceptuele consistentie. De design lead wilde dat "primary blue" en "primary green" in dezelfde rol op dezelfde intensiteit lazen, en wel in beide thema's. In sRGB of HSL is dat eigenlijk niet mogelijk. HSL beweert dat een lightness van 50% ook 50% is, maar het menselijk oog is het daar niet mee eens. Geel op HSL 50% leest bijna als wit. Blauw op HSL 50% leest bijna als zwart. Je bouwt geen serieus tokensysteem op een kleurruimte die over helderheid tegen je liegt.
De tweede reden was bereik. Hun roadmap had drie nieuwe locales — Thais, Arabisch, Chinees — met gebrande productvlakken binnen het dashboard. Brand-accenten moesten in elk thema op elk klantmonitor op dezelfde perceptuele weging zitten. OKLCH was de enige browser-native kleurruimte die we konden vinden met uniforme perceptuele lightness over hues heen. Evil Martians schreef hier de canonieke primer over, en we leunden de hele audit door op hun OKLCH picker.
Browser-support was het andere dat stilletjes was veranderd. MDN's compatibility table laat zien dat OKLCH eind 2023 in alle grote browsers zat. Toen we begonnen, draaide de klantbasis op Chromium plus Safari in door de IT uitgerolde versies. We hadden geen fallback nodig.
De token-sheet, in drie lagen
We zijn uitgekomen op een tokenmodel van drie lagen — primitives, semantische rollen, en component-slots — omdat alles platter ervan onmogelijk te refactoren is en alles dieper niet meer in één oogopslag te auditeren is.
Primitives zijn pure OKLCH-waarden, alleen benoemd naar hue-familie en stap. Ze leven als CSS custom properties op :root en verschijnen nooit in component-CSS.
:root {
/* Neutral ramp — same hue, walking lightness */
--neutral-0: oklch(98% 0.005 250);
--neutral-10: oklch(94% 0.008 250);
--neutral-20: oklch(86% 0.010 250);
--neutral-40: oklch(65% 0.014 250);
--neutral-60: oklch(45% 0.016 250);
--neutral-80: oklch(25% 0.014 250);
--neutral-90: oklch(16% 0.012 250);
--neutral-95: oklch(10% 0.010 250);
/* Brand primary — same chroma, walking lightness */
--brand-30: oklch(45% 0.18 254);
--brand-50: oklch(62% 0.20 254);
--brand-70: oklch(78% 0.16 254);
/* Signal hues — danger, warning, success — locked to L=62% */
--signal-danger: oklch(62% 0.22 28);
--signal-warning: oklch(62% 0.16 85);
--signal-success: oklch(62% 0.15 148);
}
Semantische rollen zijn wat het team daadwerkelijk gebruikt. Die verwijzen naar de primitives en flippen per thema.
[data-theme="light"] {
--surface-base: var(--neutral-0);
--surface-raised: var(--neutral-10);
--text-primary: var(--neutral-90);
--text-muted: var(--neutral-60);
--border-subtle: var(--neutral-20);
--action-primary: var(--brand-50);
}
[data-theme="dark"] {
--surface-base: var(--neutral-95);
--surface-raised: var(--neutral-90);
--text-primary: var(--neutral-10);
--text-muted: var(--neutral-40);
--border-subtle: var(--neutral-80);
--action-primary: var(--brand-70);
}
Component-slots wikkelen om de rol heen. Een knop leest --btn-bg, niet --action-primary. Daardoor kan een designer één knop bijstellen zonder de rol aan te raken en alle andere vlakken stroomafwaarts te breken.
Eén detail dat ertoe deed: brand primary gebruikt in dark mode een andere stap, geen geïnverteerde lightness. De gebruikelijke dark-mode shortcut is "flip L gewoon rond 50". Die klopt niet. Dezelfde chroma op een hogere lightness, omringd door diepe neutralen, is wat bij een nachtdienst-operator registreert als "hetzelfde blauw". De lightness inverteren geeft je een totaal andere brand-kleur.
WCAG 2.2 AA op elk vlak, niet alleen de etalage
Het oude contrastverhaal van de studio was het gebruikelijke. De hero-tekst op de marketingpagina haalde AA. De instellingenpagina haalde AA. De disabled state van het bulk-action dropdown binnen het row-actions menu van een gepagineerde tabel — waar een operator daadwerkelijk naar kijkt in uur zeven van een nachtdienst — zakte op 2.9:1.
We hebben WCAG 2.2 success criterion 1.4.3 behandeld als de ondergrens, niet als het plafond. Het criterium is 4.5:1 voor lopende tekst en 3:1 voor grote tekst en UI-componenten. We hebben contrast-checks gescript over elk rol-paar in beide thema's, daarna nogmaals over elke component-slot combinatie, en daarna nogmaals op elke disabled-state en focus-ring variant. De eerste run ving 41 niet-slagende paren. De meeste waren overgeërfd uit de day-mode token-sheet en nooit opnieuw getoetst tegen de nieuwe donkere vlakken.
De meest voorkomende dark-mode contrast-bug is de disabled state. Een 40% opacity overlay op een donker vlak komt vrijwel altijd onder 3:1 uit. Test het met de echte tokens, niet met het design-canvas.
De token-rekensom is makkelijker in OKLCH dan in hex omdat contrast nauw meeloopt met de lightness-coördinaat. Toen we wisten dat --text-muted op L=40% tegen --surface-base op L=10% ons ongeveer 5.4:1 gaf, konden we beide in stappen van vijf punten naar boven of beneden bewegen en de nieuwe ratio voorspellen binnen een halve punt. Hex-rekenwerk kan dat niet, en het designteam had het twee jaar lang met de hand gedaan.
Gevoelde snelheid, gemeten in het veld
Het klantenonderzoek had ook gevlagd: "het dashboard voelt traag 's nachts". Dat was verdacht. Zelfde software, zelfde servers, ander uur van de dag. We hebben de Chrome User Experience Report velddata getrokken voor de twee drukste routes van het dashboard, voor de vier weken vóór livegang en de vier weken erna.
Largest Contentful Paint veranderde niet noemenswaardig. Die zat al aan de groene kant van de drempel. Wat wél veranderde was Interaction to Next Paint op de twee grote tabelweergaven. Beide routes hadden de hele row-grid opnieuw gerenderd bij een theme switch. Toen het tokensysteem stond, was van thema wisselen een CSS custom-property swap op html[data-theme] — geen React re-render, geen chart redraw, geen layout shift. Velddata-INP op het 75e percentiel schoof uit de grenszone en zat de rest van het cohort comfortabel binnen de "good" band.
Dat is een les die we steeds opnieuw leren. Gevoelde snelheid is zelden een backend-verhaal. Het brein van de operator beslist binnen de eerste 200 milliseconden van een interactie of jouw app snel is, en een re-render storm tijdens een theme-wissel is genoeg om een nachtdienst-gebruiker ervan te overtuigen dat het hele product traag is.
Het cohort van twaalf weken
We zijn op een dinsdag live gegaan. De studio draait een tagged-ticket systeem in hun support-tool — elk binnenkomend ticket wordt door de dienstdoende engineer tegen een vaste woordenlijst gerouteerd.
In de twaalf weken vóór livegang leverden tickets met de tags visibility, contrast, glare, eye-strain of dark-mode gemiddeld elf per week op. Ruwweg twee klachten per dag, vooral van dezelfde vijf enterprise-accounts.
In de twaalf weken na livegang zat die groep tags op gemiddeld 1.6 per week. De daling concentreerde zich in de eerste vier weken, daarna vlak. De resterende tickets gingen vrijwel allemaal over één specifiek vlak — een third-party embedded calendar die we nog niet hadden vervangen — en uiteindelijk hebben we die leverancier uitgewisseld voor een interne build.
Twee dingen om eerlijk te benoemen. Ten eerste shipte de studio in dezelfde release ook een "reduce motion" voorkeur en een globale font-size hendel, en we kunnen het contrastwerk daar niet volledig van isoleren. Ten tweede zijn ticket-aantallen op de schaal van een studio van zes man statistisch ruisgevoelig. We claimen geen percentage. We zeggen dat het dashboard geen ding meer was waar operators over schreven.
OKLCH is geen modekeuze. Het is de kleurruimte waarin je tegelijk over contrast en brand-consistentie kunt redeneren, in code, zonder spreadsheet vol hex-waarden.
Twee dingen die we anders zouden doen
Het eerste: bouw het contrast-audit script vóór de token-sheet, niet erna. We hebben 41 paren geherrefactored omdat we het designcanvas hebben laten leiden. Was het audit-script vanaf week één de bron van waarheid geweest, dan had het designsysteem hard gefaald op het moment dat een nieuw rol-paar regresseerde, en was het herstelwerk een uur per fout geweest in plaats van een sprint.
Het tweede: lever een token-only preview-app aan de klant op vóór de migratie. De enterprise-seats van de studio hadden uitgesproken meningen over welke vlakken "hun brand" voelden versus "Microsoft Teams in het donker". Een losse preview waarin nachtdienst-leads zelf de brand primary konden swappen had twee discussies gevangen voordat ze gefactureerd herwerk werden.
De audit van vijf minuten
Als je een SaaS-product draait met een dark mode en je kunt me nu niet vertellen welke contrastverhouding je disabled-button tekst haalt op je raised-surface achtergrond, dan ship je geen dark mode. Dan ship je een Frankenstein. Open het dashboard, screenshot één drukke view, gooi 'm in welke contrast checker dan ook, en lees vier vlakken: primary text, muted text, disabled text, en de focus ring. Dat is de audit. Het kost vijf minuten en het vertelt je of de rest van dit werk een kwartaal van je team waard is.
Toen we het dashboard voor de Amsterdamse studio herbouwden, was niet het kleurrekenwerk het lastige. Het lastige was tachtig componenten herbedraden op een token-sheet zonder de workflow van één enkele bestaande klant te breken tijdens de vier weken uitrol. Dat is het soort zorgvuldige refactor dat we vaak doen onder ons webwerk, en het verdient zichzelf bijna altijd terug in de support-queue, niet in de design review.
Kern
OKLCH laat je tegelijk over contrast en brand-consistentie redeneren, in code, zonder spreadsheet vol hex-waarden.
FAQ
Waarom OKLCH in plaats van HSL of hex?
OKLCH heeft uniforme perceptuele lightness over hues heen, dus een primary blue en een primary green op dezelfde stap lezen daadwerkelijk op dezelfde intensiteit. HSL en hex geven je dat niet, waardoor contrast- en brand-rekenwerk onbetrouwbaar wordt.
Heeft OKLCH browser-support in 2026?
Ja. Elke evergreen browser shipt het sinds eind 2023. Als je publiek op Chromium en Safari in door IT uitgerolde versies zit, heb je geen sRGB-fallback nodig.
Welke contrastverhouding moet een donker dashboard halen?
WCAG 2.2 AA: 4.5:1 voor lopende tekst, 3:1 voor grote tekst en UI-componenten zoals knoppen en focus rings. Test elk disabled-state paar ook, niet alleen de etalage-vlakken.
Hoe lang duurt een token-herbouw voor een SaaS-dashboard van middelgrote omvang?
Voor ongeveer tachtig componenten en twee thema's reken op zes tot tien weken met één designer en twee engineers erop. Eerst audit-script, dan primitives, rollen, slots, dan uitrol in flights.