HEX, RGB, HSL en de nieuwere kleurruimtes

Hoe HEX, RGB, HSL, LCH en OKLCH zich tot elkaar verhouden, waarom een HSL-palet in de middentonen modderig wordt, en wat gamut clipping en alpha compositing doen met de kleur die je krijgt.

Een kleur in CSS is drie of vier getallen plus een regel om ze te lezen: de kleurruimte. Paletten die op het scherm ongelijkmatig ogen, zijn meestal opgebouwd in een ruimte die niet aansluit op hoe het zicht werkt.

HEX en RGB zijn dezelfde getallen

#3B82F6 is drie bytes in grondtal 16: 3B is 59, 82 is 130, F6 is 246, dus rgb(59, 130, 246) is dezelfde kleur in grondtal 10, en een kleurconverter wisselt ertussen zonder verlies.

De verkorte notatie van drie cijfers verdubbelt elk cijfer in plaats van het aan te vullen, dus #F80 wordt #FF8800, niet #F08000; slechts 4.096 kleuren hebben een korte vorm. #RGBA voegt op dezelfde manier een alfacijfer toe en #RRGGBBAA schrijft alle vier de bytes uit, waarbij die alfabyte van 0 tot 255 loopt (1A is 10%, 80 is 50%, CC is 80%) terwijl elke andere alfa in CSS van 0 tot 1 loopt. De moderne syntaxis accepteert ook componenten gescheiden door spaties met de alfa achter een schuine streep: rgb(59 130 246 / 50%).

Deze getallen zijn geen hoeveelheden licht. sRGB codeert elk kanaal via een overdrachtscurve van ruwweg 2,2, dus 128 straalt ongeveer 21,6% uit van het licht dat 255 uitstraalt, niet de helft, en elke menging die op de ruwe waarden wordt berekend erft die curve.

HSL, HSV en de lichtheid die geen lichtheid is

HSL herschikt dezelfde RGB-kubus tot een cilinder: hue is een hoek (0 rood, 120 groen, 240 blauw), saturation is de afstand tot de centrale grijsas, en lightness loopt van 0 via 50%, de zuivere kleurtoon, naar 100%, wit. HSV vervangt lightness door value, waarvan de bovenkant de zuivere kleurtoon is in plaats van wit, zodat lichte varianten ontstaan door saturation te verlagen. Geen van beide weet iets van waarneming.

KleurHSLRelatieve luminantieOKLCH-lichtheid
#FFFF00hsl(60 100% 50%)0,9280,97
#0000FFhsl(240 100% 50%)0,0720,45

Beide claimen 50% lichtheid; de een straalt ongeveer dertien keer zoveel licht uit als de ander. Een rij tokens waarin elke kleurtoon op 50% is vastgezet, leest als een allegaartje van gewichten.

De lightness in HSL in stappen verlagen bij vaste hue en saturation faalt om dezelfde reden, plus nog een: saturation wordt gemeten tegen de ruimte die bij die lichtheid beschikbaar is, dus de stap op 50% is de meest kleurrijke en alles aan weerszijden verbleekt. De hoek van de kleurtoon blijft ook staan terwijl de waargenomen chroma instort, en dat maakt de middelste stappen modderig. Het zijn niet donkerder gemaakte basiskleuren, het zijn basiskleuren die met een door niemand gekozen hoeveelheid vergrijsd zijn.

Geel laat het ergste ervan zien. De HSL-lichtheid verlagen loodst geel regelrecht naar olijfgroen, want een donker geel bij die kleurtoonhoek ís olijfgroen, en het geel houden betekent dat je de kleurtoon onderweg naar beneden richting oranje draait. Blauw heeft het omgekeerde probleem en zit zo laag in luminantie dat alle bruikbare variatie boven de 50% ligt.

LCH en OKLCH

LCH is de poolvorm van CIE Lab, opgebouwd uit kleurvergelijkingsexperimenten: lichtheid 0 tot 100, chroma (kleurrijkheid, zonder vast maximum) en een kleurtoonhoek. OKLCH heeft dezelfde vorm maar dan over Oklab, dat de kleurtoonverschuivingen corrigeert waar Lab om bekendstaat, het duidelijkst blauwtinten die naar paars afdrijven naarmate ze lichter worden.

.a { color: oklch(0.62 0.21 260); }        /* L 0 to 1, C about 0 to 0.37, H in degrees */
.b { color: oklch(62% 0.21 260 / 50%); }   /* percentages and alpha both allowed */
.c { color: lch(52% 82 285); }             /* L runs 0 to 100 here */

Perceptuele uniformiteit betekent dat gelijke numerieke afstand dicht bij gelijke waargenomen afstand ligt. Een verloop in stappen van 0,08 lichtheid oogt als gelijke stappen, een rij kleurtonen die op één lichtheid is vastgezet leest als één rij, en de lichtheid veranderen laat de kleurtoon staan waar je hem gezet hebt. Interpolatie kan de ruimte ook gebruiken, zoals in linear-gradient(in oklch, #0000FF, #FFFF00). Eén waarschuwing: de lichtheid in OKLCH ligt veel dichter bij de waargenomen helderheid dan die in HSL, maar het is niet de luminantieformule van WCAG, dus tekstcombinaties gaan nog steeds door een contrastchecker.

Gamut en clipping

Een gamut is de verzameling kleuren die een ruimte kan weergeven. sRGB is de al lang gangbare standaard; Display P3 dekt ruwweg een kwart meer van de zichtbare kleuren, waarbij vrijwel de hele winst in diepe rood- en groentinten zit. Chroma in OKLCH kent geen bovengrens, dus een kleur opschrijven die geen enkel scherm kan tonen is zo gebeurd.

@supports (color: color(display-p3 1 1 1)) {
  .vivid { background: color(display-p3 0.9 0.1 0.14); }
}

Clipping per kanaal haalt een kleur buiten het gamut terug door elk kanaal onafhankelijk op zijn grens vast te zetten, wat de kleurtoon verschuift en naburige tinten op één waarde platslaat, waardoor kleurverlopen banden krijgen en detail verdwijnt. CSS Color 4 doet in plaats daarvan aan mapping: houd lichtheid en kleurtoon vast en verlaag de chroma in OKLCH tot de kleur past.

Daarom kan een schermafbeelding van een felle foto vlak ogen. De foto draagt een P3-profiel en het scherm toont hem volledig; een export die naar sRGB is omgezet perst die verzadigde pixels in een kleiner volume, en rood en groen moeten daarbij het verst reizen.

Alpha compositing

Een halftransparante kleur over een achtergrond levert een derde, ondoorzichtige kleur op, kanaal voor kanaal:

result = alpha * source + (1 - alpha) * backdrop

Zwart op 50% over wit geeft (127.5, 127.5, 127.5), oftewel #808080. Dat toeval is waar "50% dekking van zwart is grijs" vandaan komt, en het klopt op wit en nergens anders: hetzelfde zwart over #3B82F6 geeft ongeveer #1E417B, een donkerblauw, en over een foto geeft het in elke pixel een andere kleur. Halftransparant zwart is een verdonkerende bewerking, geen grijs.

Compositing rekent met de gecodeerde waarden, dus die #808080 heeft een relatieve luminantie van ongeveer 0,216 in plaats van 0,5. Meet de samengestelde hexwaarde, nooit de opgegeven kleur. Alfa stapelt ook niet zoals verwacht: twee lagen van 50% dekken samen 75%, omdat de tweede alleen inwerkt op wat de eerste doorliet.

Tints, shades, tones en harmonieën

TermTraditioneel receptIn OKLCH
Tintbasis plus witL omhoog, C iets omlaag
Shadebasis plus zwartL omlaag
Tonebasis plus grijsC omlaag bij gelijke L

Harmonieën zijn verschuivingen van de kleurtoon. Ze kiezen een accent; ze bouwen geen systeem.

HarmonieVerschuivingGoed voor
Complementair+180°één accent tegen een overheersende kleurtoon, al gaat het trillen als beide verzadigd zijn
Analoog±30°rustige groepen, achtergronden, toestanden binnen één familie
Triadisch+120°, +240°categorische verzamelingen zoals reeksen in een grafiek

Verschuivingen op de RGB-cirkel zijn niet perceptueel gelijkmatig, omdat die cirkel een onevenredig grote boog aan groen besteedt, dus behandel een berekende harmonie als een eerste opzet en corrigeer op het oog. Een paletgenerator die tints, shades en harmonieën uit één basiskleur bouwt, zet die opzet snel op het scherm.

Het neutrale verloop vraagt om een eigen beslissing. Een puur grijs, met rood, groen en blauw gelijk, oogt naast een gekleurde interface dood en licht vuil. Geef de neutrale tinten een kleine chroma, ruwweg 0,005 tot 0,02 in OKLCH, in de buurt van de merkkleurtoon: warme grijstinten rond 60° tot 90° lezen als papier, koele grijstinten rond 250° lezen technisch. Bouw die chroma af richting de lichtste stappen, zodat achtergronden niet gekleurd lijken.

Kleurenblindheid

Ongeveer 1 op de 12 mannen en 1 op de 200 vrouwen heeft een kleurzienstoornis. De rood-groentypen vormen vrijwel het geheel, met deuteranomalie ruimschoots voorop; de blauw-gele tritantypen komen bij minder dan 1 op de 10.000 voor.

De verwarringen die er in interfaces toe doen zijn rood tegen groen bij vergelijkbare lichtheid, groen tegen bruin en oranje, paars tegen blauw, en roze tegen grijs. Protaan zicht maakt rood bovendien donkerder, dus een rood dat voor urgentie is gekozen kan het stilste ding op het scherm worden.

Kleur mag daarom nooit de enige drager van betekenis zijn. Koppel elke toestand aan een label, een pictogram, een vorm of een positie, en laat gekoppelde toestanden ook in lichtheid verschillen en niet alleen in kleurtoon, want een rood en een groen met dezelfde OKLCH-lichtheid vormen de slechtst denkbare combinatie. Een schermafbeelding naar grijstinten omzetten legt zulke botsingen in seconden bloot.