Les harmonies de couleurs sont de la géométrie sur une roue — et la roue est fausse
Publié le 15/08/2025 · 16 min de lecture · Outils pour développeurs
Daniel Okonkwo — Développeur front-end et rédacteur Tech chez OneKitly
Performance web · Formats de fichiers
Vérifié à partir de 6 sources
Une harmonie de couleurs est un angle. Prends la teinte d'une couleur de base, ajoute un nombre fixe de degrés modulo 360, et tu as l'harmonie : la complémentaire ajoute 180°, la triadique ajoute deux fois 120°, l'analogue ajoute ±30°, la complémentaire divisée ajoute 150° et 210°, la tétradique ajoute trois fois 90°. Depuis la teinte 210, cela donne 30 pour la complémentaire, 330 et 90 pour la triade, 180 et 240 pour les voisines, 0 et 60 pour la divisée. C'est tout le calcul, et tous les générateurs le font. L'ennui, c'est le cercle sur lequel on tourne. L'axe de teinte de HSL est une construction géométrique sur le cube RVB, pas un modèle de la vision : les angles égaux ne sont pas des pas perceptuels égaux, et une clarté HSL égale n'est pas une clarté perçue égale. Rendus en hsl(H 100% 50%), le jaune a une luminance relative WCAG de 0,9278 et le bleu 0,0722 — même clarté nominale, 12,85 fois la lumière. OKLCH et CIELAB corrigent cela en définissant la clarté perceptuellement : la même paire vaut 0,968 contre 0,452 en OKLCH, un rapport de 2,14. La conséquence pratique est inévitable : la rotation ne préserve rien du contraste, donc une palette générée doit être vérifiée face à ses fonds avant d'être livrée.
Complémentaire, triadique, analogue et complémentaire divisée ne sont que des rotations : ajoute 180°, 120°, 30° ou 150° à une teinte. L'arithmétique est triviale. Le problème, c'est que le cercle de teintes HSL n'est pas perceptuellement uniforme — le jaune et le bleu à la même clarté HSL diffèrent d'un facteur 12,85 en luminance — donc une palette générée doit être vérifiée en contraste ensuite.
Les quatre harmonies sont quatre additions
Toute harmonie nommée est un jeu de décalages appliqués à un angle de teinte. La complémentaire vaut base + 180. La triadique vaut base, base + 120, base + 240. L'analogue vaut base − 30, base, base + 30, même si certains outils utilisent 20 ou 45. La complémentaire divisée vaut base + 150 et base + 210, c'est-à-dire la complémentaire écartée de 30° de chaque côté. La tétradique, dite aussi carrée, vaut base + 90, + 180 et + 270. La monochromatique ne change aucune teinte et ne fait varier que la saturation et la clarté.
Prenons un exemple. Soit un bleu moyen à la teinte 210. La complémentaire donne (210 + 180) mod 360 = 30, un orange. La triadique donne 210, (210 + 120) mod 360 = 330 et (210 + 240) mod 360 = 90 — bleu, rose et vert-jaune. L'analogue donne 180, 210, 240 — cyan, bleu, bleu-violet. La complémentaire divisée donne (210 + 150) mod 360 = 0 et (210 + 210) mod 360 = 60 — rouge et jaune. La tétradique donne 210, 300, 30, 120. L'algorithme n'a rien d'autre ; le modulo est la seule subtilité, et il n'existe que parce que le cercle boucle.
Comme les décalages ne touchent que la teinte, la saturation et la clarté sont reportées telles quelles. C'est exactement ce qui donne à la sortie son air de famille — et exactement ce qui provoque l'échec décrit plus bas, car maintenir S et L constants ne maintient constant rien de ce que mesurent l'œil humain ou un vérificateur d'accessibilité.
Il y a deux roues, et elles ne s'accordent pas sur le bleu
Demande à un peintre ce qui est opposé au bleu : il répondra l'orange. Demande-le à un générateur d'harmonies : il répondra le jaune. Les deux ont raison, car ils tournent sur des cercles différents. La roue du peintre a le rouge, le jaune et le bleu pour primaires — le modèle RJB, systématisé par Itten et enseigné depuis dans les écoles d'art — et elle décrit le mélange soustractif des pigments. La roue du générateur est l'axe de teinte de HSL, une reparamétrisation cylindrique du cube RVB additif, dont les primaires sont rouge, vert et bleu et les secondaires cyan, magenta et jaune.
Les nombres rendent la fracture concrète. En projetant le cube RJB sur le sRGB par l'interpolation trilinéaire classique, le bleu primaire RJB tombe près de #2a5f99 et l'orange RJB à #ff8000, et ils occupent des coins opposés de ce modèle. Sur la roue HSL, le bleu est la teinte 240 et 240 + 180 = 60, ce qui rend #ffff00, un jaune pur. Aucune roue ne ment. Le RJB décrit ce qui arrive quand on mélange de la peinture ; le HSL décrit ce qui arrive quand on mélange de la lumière. Ce sont des modèles de processus physiques différents, et rien n'oblige leurs opposés à coïncider.
Cela mérite d'être su avant de contester la sortie d'un générateur. Si une référence de design te dit que le complément de ton bleu de marque est un orange chaud et que l'outil te rend un jaune citron, personne ne s'est trompé — tu lis simplement deux systèmes de coordonnées. Si tu veux la réponse picturale, certains générateurs offrent un mode RJB ; sinon, tourne de 30° à 40° en deçà de la complémentaire mathématique et tu atterris dans la zone orange.
Une clarté HSL égale n'est pas une lumière égale
Voici la démonstration. Rendez neuf teintes à exactement hsl(H 100% 50%) — saturation identique, clarté nominale identique — et passe chacune dans le calcul de luminance relative du WCAG : divise chaque canal par 255, linéarise-le et pondère les résultats 0,2126 rouge, 0,7152 vert, 0,0722 bleu. La teinte 60 donne #ffff00 avec une luminance relative de 0,9278. La teinte 240 donne #0000ff avec 0,0722. Même curseur de clarté, 12,85 fois la lumière.
Les conséquences sont immédiatement visibles en termes de contraste. Sur fond blanc, ce jaune obtient 1,07:1 — invisible — tandis que ce bleu obtient 8,59:1 et franchit tous les niveaux WCAG. L'un contre l'autre, la paire obtient 8,00:1, d'où le jaune sur bleu comme combinaison classique de signalétique et le jaune sur blanc comme erreur classique. Le balayage complet est instructif : la teinte 0 (#ff0000) a une luminance de 0,2126, la teinte 120 (#00ff00) de 0,7152 et la teinte 180 (#00ffff) de 0,7874. L'étendue sur un seul anneau de clarté couvre presque toute la plage de 0 à 1.
La raison est dans les poids. La luminance relative est dominée par le vert, qui en porte 71,52 %, tandis que le bleu en porte 7,22 %. La clarté de HSL n'en sait rien : elle est définie comme le milieu entre le plus grand et le plus petit canal RVB, une grandeur purement géométrique. À L = 50 % et S = 100 %, toute teinte est par construction une couleur dont les canaux couvrent toute la plage 0 à 255, et la quantité de lumière que cela représente dépend entièrement des canaux allumés.
Ce que OKLCH et CIELAB appellent clarté
CIELAB, publié par la CIE en 1976, fut la première tentative largement adoptée d'espace perceptuellement uniforme. Son axe L* va de 0 à 100 et repose sur une compression en racine cubique de la luminance, choisie pour que des pas numériques égaux paraissent des pas égaux. Passe-y le même jaune et le même bleu : L* = 97,14 pour #ffff00 et 32,30 pour #0000ff. Toujours très différents, mais la différence est maintenant honnête — le jaune paraît réellement bien plus clair — et le rapport est tombé de 12,85 à 3,01.
OKLab, publié par Björn Ottosson en 2020 et désormais intégré à CSS Color 4 sous oklab() et oklch(), améliore CIELAB là où CIELAB est réputé faible : la constance de teinte pour les bleus et la prévisibilité sous changement de clarté. Son L va de 0 à 1. Le jaune vaut 0,968, le bleu 0,452. Sa forme polaire, OKLCH, te donne clarté, chroma et teinte — les trois mêmes notions que HSL prétend offrir, mais dont les deux premières signifient enfin ce qu'elles disent. C'est important, car une palette générée à clarté OKLCH fixe ressemble réellement à une famille de couleurs de même luminosité, exactement ce que les designers croyaient demander à HSL.
Une réserve mérite d'être dite clairement. La clarté perceptuelle n'est toujours pas la luminance relative, et ni le L de OKLCH ni le L* de CIELAB ne sont ce que calcule un vérificateur WCAG. Deux couleurs de clarté OKLCH identique peuvent avoir des luminances relatives différentes : égaliser la clarté perceptuellement n'égalise donc pas les rapports de contraste. Cela te donne une palette qui paraît cohérente ; cela ne te donne pas une palette conforme.
Des rotations égales ne sont pas des pas égaux
Le problème de clarté a un jumeau sur l'axe de teinte. Une rotation de 30° dans la zone verte — de la teinte 90 à la teinte 120 — te fait passer entre deux verts que la plupart des gens peineraient à distinguer. Une rotation de 30° dans la zone bleu-magenta — de 240 à 270 — te fait passer du bleu pur à un violet nettement différent. Les angles sont identiques ; les distances perçues ne le sont pas, car la teinte de HSL dérive des arêtes du cube RVB et le cube n'est pas une sphère perceptuelle.
Vois ce que cela fait à une triade. Une rotation de 120° depuis la teinte 0 donne #ff0000, #00ff00 et #0000ff, de luminances relatives 0,2126, 0,7152 et 0,0722 et de clartés OKLCH 0,628, 0,866 et 0,452. Un membre est presque deux fois plus clair qu'un autre en termes perceptuels et dix fois plus en termes physiques. Fais tourner la même triade depuis la teinte 90 et tu obtiens #80ff00, #0080ff et #ff0080, de clartés OKLCH 0,891, 0,615 et 0,645 — un équilibre entièrement différent, issu de la même construction, avec pour seul changement l'angle de départ.
C'est pourquoi une triade paraît tantôt équilibrée, tantôt comme une couleur qui hurle par-dessus deux autres, sans que la méthode change. La méthode est aveugle à cela. Générer la même harmonie en OKLCH — tourner la teinte, garder L et C — supprime l'essentiel de la variance, car la clarté que tu maintiens est désormais celle que tu vois.
La géométrie ne connaît rien au contraste
Prends un vrai bleu de marque, #2563eb. En HSL c'est hsl(221.2 83.2% 53.3%), et sur blanc il obtient 5,17:1 — confortablement au-delà du seuil de 4,5:1 pour le texte courant en WCAG AA. Génère maintenant sa famille de la façon habituelle, en tournant la teinte à saturation et clarté constantes. Le partenaire triadique à +120° est #eb2563 et obtient 4,22:1 : échec. La complémentaire divisée à +150° est #eb4a25, 3,79:1 : échec. La complémentaire à +180° est #ebad25, 1,99:1 : échec. À +210°, c'est #c6eb25, 1,37:1 : échec. À +240°, #63eb25, 1,56:1 : échec. Cinq rotations, cinq échecs, à partir d'une base conforme.
Rien n'a mal tourné. Le générateur a fait exactement ce qu'il annonce : il a tourné une teinte. Le contraste est une propriété d'une paire de couleurs, et un générateur d'harmonies reçoit une couleur et un angle. Il n'a aucun fond de comparaison et aucun moyen de savoir laquelle de ses sorties finira en texte et laquelle en aplat décoratif. Attendre une palette accessible d'une rotation, c'est attendre la réponse à une question qui n'a jamais été posée.
Le processus qui marche compte deux étapes, pas une. Génère la géométrie pour trouver des teintes qui vont ensemble, puis ajuste la clarté couleur par couleur jusqu'à ce que chacune franchisse le rapport dont elle a besoin face à la surface où elle se posera. En pratique, cela veut dire garder la teinte et le chroma donnés par le générateur et ne bouger que la clarté — exactement l'opération que OKLCH rend prévisible et que HSL ne rend pas. Les rôles de texte exigent 4,5:1, le grand texte et les bordures d'interface 3:1, et les aplats décoratifs rien du tout : la plupart des palettes finissent donc avec deux variantes de clarté par teinte plutôt qu'une.
| Teinte | sRGB | Luminance relative WCAG | Contraste sur blanc | L OKLCH | L* CIELAB |
|---|---|---|---|---|---|
| 60 (jaune) | #ffff00 | 0,9278 | 1,07:1 | 0,968 | 97,14 |
| 180 (cyan) | #00ffff | 0,7874 | 1,25:1 | 0,905 | 91,11 |
| 120 (vert) | #00ff00 | 0,7152 | 1,37:1 | 0,866 | 87,73 |
| 30 (orange) | #ff8000 | 0,3670 | 2,52:1 | 0,732 | 67,05 |
| 300 (magenta) | #ff00ff | 0,2848 | 3,14:1 | 0,702 | 60,32 |
| 0 (rouge) | #ff0000 | 0,2126 | 4,00:1 | 0,628 | 53,24 |
| 270 (violet) | #8000ff | 0,1181 | 6,25:1 | 0,531 | 40,91 |
| 240 (bleu) | #0000ff | 0,0722 | 8,59:1 | 0,452 | 32,30 |
Questions fréquentes
- Pourquoi ma palette triadique paraît-elle déséquilibrée alors que les maths sont symétriques ?
- Parce que la symétrie est dans les angles, pas dans l'apparence. Une triade tourne deux fois de 120°, ce qui est parfaitement régulier sur le cercle des teintes, mais ce cercle n'est pas perceptuellement régulier. Depuis la teinte 0 en hsl(H 100% 50%), on obtient #ff0000, #00ff00 et #0000ff, de luminances relatives 0,2126, 0,7152 et 0,0722 — le vert émet dix fois la lumière du bleu à clarté HSL identique. En OKLCH, les clartés valent 0,628, 0,866 et 0,452, soit encore un écart de près de deux pour un. Le membre qui tombe dans l'arc vert ou jaune dominera, car ces teintes portent l'essentiel de la luminance en sRGB. Le remède n'est pas d'abandonner la triade mais d'égaliser ensuite : convertis les trois en OKLCH, fixe-leur le même L et re-rendez. Les teintes restent où la géométrie les a mises et le poids visuel s'égalise. Si ton outil ne sait pas faire cela, ajuster la clarté HSL à l'œil couleur par couleur te mène presque au but — simplement, tu ne peux pas le faire en gardant un nombre constant.
- Le complément du bleu est-il l'orange ou le jaune ?
- Les deux, selon la roue sur laquelle tu te tiens. Sur la roue RJB des artistes — rouge, jaune, bleu comme primaires, le modèle systématisé par Itten et toujours enseigné en école d'art — le bleu est opposé à l'orange, car l'orange est ce qu'on obtient en mélangeant les deux autres primaires. Sur la roue RVB qu'expose HSL, le bleu est la teinte 240, son opposé est la teinte 60, et la teinte 60 à pleine saturation est #ffff00, un jaune pur. Projeter le cube RJB sur le sRGB place le bleu RJB près de #2a5f99 et l'orange RJB à #ff8000, et ce sont bien des coins opposés de ce modèle. Aucun n'a tort. Le RJB modélise le mélange soustractif des pigments, où bleu et orange s'annulent vers un brun neutre ; le RVB modélise le mélange additif de la lumière, où lumière bleue et lumière jaune s'additionnent en blanc. Les logiciels travaillent dans la lumière : la complémentaire d'un générateur sera donc le jaune. Pour le résultat pictural, utilise un outil avec mode RJB ou tourne de 30° à 40° en deçà de la complémentaire mathématique — depuis la teinte 240 tu atterris vers la teinte 30, qui rend #ff8000, l'orange attendu.
- Faut-il générer les palettes en OKLCH plutôt qu'en HSL ?
- Pour tout ce dont les membres doivent se ressentir comme des pairs, oui. Maintenir la clarté OKLCH constante au fil d'une rotation donne des couleurs qui paraissent réellement également claires, là où maintenir la clarté HSL constante donne l'écart de luminance d'un facteur 12,85 entre jaune et bleu décrit plus haut. OKLCH est pris en charge en CSS via oklch() et fait partie de la spécification CSS Color 4 : tu peux donc l'écrire directement plutôt que convertir. Deux précautions pratiques. D'abord, OKLCH décrit des couleurs hors du gamut sRGB : un chroma qui passe à une teinte peut être inatteignable à une autre — les navigateurs font du gamut mapping, mais le résultat aura moins de chroma que demandé, et une palette définie à un seul C élevé ne sortira pas uniformément saturée. Reste conservateur, autour de 0,10 à 0,15, si chaque teinte doit survivre. Ensuite, la clarté OKLCH n'est toujours pas la luminance relative du WCAG : des palettes à L égal ne sont pas des palettes à contraste égal. Il faut toujours vérifier chaque couleur face à chaque fond qu'elle rencontrera. Ce que OKLCH t'apporte, c'est que la vérification passera ou échouera généralement pour tous les membres ensemble, plutôt qu'un par un pour des raisons inexplicables.
- Une rotation de teinte plus grande signifie-t-elle toujours plus de contraste ?
- Non, et les contre-exemples sont faciles à produire. Le contraste selon le WCAG ne dépend que de la luminance relative, et la rotation de teinte modifie la luminance sans rapport avec l'ampleur de la rotation. Depuis #2563eb, la rotation qui abîme le moins le contraste est +120° à 4,22:1, et celle qui l'abîme le plus est +210° à 1,37:1 — mais +180°, la plus grande rotation possible, atterrit à 1,99:1, entre les deux. La relation n'est pas monotone car les poids de luminance culminent brutalement dans l'arc vert-jaune : toute rotation qui t'emmène dans la région des teintes 60 à 120 élèvera la luminance et, sur fond blanc, abaissera le contraste, que tu y sois arrivé en tournant de 90° ou de 250°. Le bon modèle mental : la rotation de teinte est une marche sur un relief dont l'altitude est la luminance, avec une montagne vers le vert-jaune et une vallée vers le bleu. La distance parcourue ne dit rien de l'altitude atteinte. Si deux couleurs doivent contraster, écarte délibérément leurs clartés ; tourner la teinte seule n'est pas une stratégie de contraste, c'est une stratégie de teinte.
- De combien de couleurs une harmonie a-t-elle réellement besoin ?
- Moins de teintes et plus de paliers de clarté que ne le suggèrent la plupart des générateurs. Une palette d'interface fonctionnelle, c'est typiquement une teinte de marque, une rampe neutre et au plus un accent, chacun existant en plusieurs variantes de clarté — une teinte claire de surface, un aplat moyen et une version sombre utilisable en texte. Cette dernière explique la croissance du décompte : comme montré plus haut, une même teinte à une même clarté ne peut servir à la fois de fond lavé et de texte lisible, car l'un de ces rôles exige 4,5:1 face au blanc et l'autre doit y être à peine visible. Le rôle du générateur est de trouver les relations de teinte ; le vôtre est de donner à chaque teinte les deux ou trois paliers de clarté que ses rôles réclament. Une harmonie à quatre teintes avec une variante chacune échouera quelque part ; une harmonie à deux teintes avec cinq variantes chacune, non. Si tu comptes, vise une rampe neutre à neuf paliers, cinq paliers de la teinte de marque et cinq de l'accent, et vérifie ceux destinés au texte face à toutes les surfaces où ils peuvent apparaître.
Articles qui pourraient t'intéresser
Tous les guides →Outils similaires
Sources
- W3C — CSS Color Module Level 4 — hue interpolation, oklab() and oklch(), and the HSL to RGB algorithm
- W3C — Web Content Accessibility Guidelines (WCAG) 2.2 — relative luminance and contrast ratio definitions
- CIE — CIE 015:2018 Colorimetry, 4th Edition — CIELAB and the L* lightness function
- Björn Ottosson — A perceptual color space for image processing (OKLab), 2020
- Charles Poynton — Color FAQ and Gamma FAQ — luminance, lightness and the difference between them
- IEC — IEC 61966-2-1:1999 — Default RGB colour space (sRGB), source of the piecewise transfer function
Tu as repéré une erreur dans cet article ?