Skip to content
Retour au blog
Tutoriels

OKLCH expliqué — pourquoi Tailwind v4 l'a adopté

Pourquoi OKLCH est devenu le standard des systèmes de design en 2024–2026. En quoi il diffère de HSL et LCH, avec une conversion HEX→OKLCH détaillée pas à pas.

14 min de lecture

L’espace colorimétrique OKLCH expliqué — Pourquoi Tailwind v4 l’a adopté

Ouvrez le source de n’importe quel système de design de l’ère 2025 — shadcn/ui, Radix Themes, la palette de base de Tailwind v4 — et la première chose qui saute aux yeux, ce sont les couleurs. Pas des codes hex, pas des triplets hsl(), mais une fonction dont personne ne parlait il y a trois ans : oklch(). Tailwind v4 livre l’intégralité de sa palette par défaut sous forme de littéraux OKLCH. shadcn génère désormais des thèmes qui émettent des propriétés personnalisées OKLCH. Le système de design de Vercel s’est reconstruit autour en 2024.

Ce n’est pas un effet de mode. Il y a une raison spécifique, mathématique, pour laquelle chaque système de design sérieux a discrètement basculé de modèle colorimétrique — et une fois que vous l’avez vue, vous ne pouvez plus ignorer pourquoi HSL a toujours été le mauvais outil pour ce à quoi nous l’utilisions.

Cet article parcourt cette raison depuis les premiers principes, se termine par une conversion détaillée d’un code hex en OKLCH, et vous donne une recette de migration pour votre propre palette.

Quand les espaces colorimétriques ont cassé

Les systèmes de design ont un travail tonal. Un bouton survolé est légèrement plus clair que son état au repos. Une carte atténuée se place un cran plus foncée que la surface qui l’entoure. Un anneau de focus doit être visiblement plus lumineux que le chrome neutre derrière lui. Bien faire ce travail, à grande échelle, exige que « plus clair » et « plus foncé » signifient la même chose sur toutes les teintes de votre palette.

Cette exigence était facile à ignorer quand les palettes comportaient huit couleurs et trois états. Elle est devenue inconfortable quand les équipes ont commencé à livrer des rampes à 11 paliers (50–950 dans la convention de Tailwind), huit couleurs sémantiques, des variantes claires et foncées, et des accents de marque qui devaient cohabiter avec les couleurs système d’iOS, d’Android et du Web. Soudain, la question « ce teal-500 a-t-il la même clarté perceptuelle que notre blue-500 » est devenue un vrai problème d’ingénierie, plus un luxe de direction artistique.

HSL — le cheval de trait depuis CSS 3 — ne pouvait pas y répondre. Deux couleurs HSL avec des valeurs L identiques peuvent paraître radicalement différentes en luminosité perçue. Un jaune HSL pur à lightness: 50% paraît bien plus lumineux qu’un bleu HSL pur à la même clarté perceptuelle. Vos yeux ne perçoivent pas le jaune et le bleu de la même manière ; HSL a été conçu pour être intuitif pour les sélecteurs, pas perceptuellement cohérent pour les rampes. En 2023, chaque système de design qui dépassait une poignée de couleurs corrigeait ce problème avec des scripts de mélange personnalisés ou des dérogations réglées à la main.

Ce qu’il nous fallait, c’était un modèle colorimétrique où L signifie effectivement « la luminosité perçue qu’un humain reporterait », et où faire pivoter la teinte ou réduire la saturation ne modifie pas invisiblement la luminosité comme effet de bord. Ce modèle existait dans la science colorimétrique académique — il n’avait juste pas encore atteint CSS.

Le problème HSL, concrètement

Déposez ceci dans un navigateur et regardez-les côte à côte :

.a { background: hsl(60 100% 50%); }   /* yellow */
.b { background: hsl(240 100% 50%); }  /* blue */
.c { background: hsl(120 100% 50%); }  /* green */

Les trois ont L: 50%. Aucun ne paraît avoir la même clarté perceptuelle. Le jaune brûle presque ; le bleu se lit presque noir sur une page blanche ; le vert se place entre les deux. Si vous construisez un état de survol en ajoutant 10% à L, le survol du jaune est à peine visible tandis que le survol du bleu est un changement spectaculaire. Le raffinement de vos interactions finit par dépendre de la teinte avec laquelle le designer a commencé.

Ce n’est pas un bogue de HSL. HSL a été conçu en 1978 pour des sélecteurs de couleur de type peinture-au-numéro, où les utilisateurs manipulaient la teinte, la saturation et la « clarté perceptuelle » — définie comme (max(R,G,B) + min(R,G,B)) / 2 — pour régler une couleur. Les mathématiques n’ont aucune notion de perception humaine. La clarté perceptuelle dans HSL est un point médian géométrique des canaux sRGB, rien de plus.

La CIE — l’organisme international de normalisation pour la colorimétrie — connaissait ce problème depuis les années 1970. Elle a publié deux espaces perceptuellement uniformes, CIELAB et CIELUV, qui définissaient la clarté perceptuelle comme quelque chose de plus proche de ce que fait réellement la vision humaine. Dans les années 1990, CIE LAB était devenu le standard en imprimerie, photographie et gestion des couleurs. Mais sa conversion vers RGB est ardue, et CSS ne l’a jamais adopté largement. Les développeurs web ont continué à utiliser HSL non pas parce qu’il était juste, mais parce qu’il était là.

CIE LAB / LCH : la correction académique, avec ses propres problèmes

CIELAB prend une valeur tristimulus XYZ (un modèle de la façon dont les cônes humains répondent à la lumière) et la fait passer par une racine cubique et une rotation 2D pour produire trois canaux : L* (clarté perceptuelle, 0–100), a* (vert ↔ rouge) et b* (bleu ↔ jaune). LCH est le même espace exprimé sous forme polaire : L*, C* (chroma, distance au neutre), H* (angle de teinte).

Ces espaces sont perceptuellement uniformes au sens mesurable. Un ΔE de 1 — un pas unitaire dans n’importe quelle direction de l’espace LAB — correspond approximativement à la plus petite différence de couleur qu’un observateur entraîné peut détecter. Les workflows d’imprimerie et de prépresse fonctionnent sur LAB et LCH depuis des décennies.

Alors pourquoi CSS n’a-t-il pas simplement adopté LCH pour passer à autre chose ?

Deux raisons. Premièrement, CIE LAB a été calibré contre une condition de visualisation spécifique (un observateur standard à 2° sous illumination D50) optimisée pour la réflectance de surface, pas pour les écrans émissifs. Sur les écrans, son uniformité perceptuelle dérive — des couleurs « également lumineuses » en LAB ne paraissent pas toujours également lumineuses sur un téléphone. Deuxièmement, le gamut chromatique de LCH est pénible. Il existe des couleurs visibles que LAB décrit bien mais qui se trouvent en dehors des gamuts chromatiques d’affichage courants, et la projection de LCH vers sRGB produit parfois des décalages de teinte (votre bleu vire légèrement au violet quand vous réduisez son chroma). Pour le travail de systèmes de design, ce sont deux obstacles rédhibitoires.

CSS Color 4 a bien ajouté lab() et lch() en 2021, et ils fonctionnent dans les navigateurs modernes. Mais pour le problème spécifique de la construction de rampes tonales cohérentes sur des écrans émissifs, la communauté a continué à chercher.

OKLAB / OKLCH : l’intuition d’Ottosson en 2020

En décembre 2020, Björn Ottosson — un ingénieur couleur suédois — a publié un article intitulé « A perceptual color space for image processing ». L’article était court : trois petites matrices, une étape de racine cubique, pas de tables de calibration, pas de données de référence sous copyright. Ottosson a pris les modèles colorimétriques IPT et CAM16-UCS existants — des espaces académiques aux bonnes propriétés mais aux mauvaises mathématiques — et en a dérivé un espace plus simple qui approximait leur comportement perceptuel en utilisant des multiplications matricielles ordinaires sur des valeurs tristimulus XYZ en lumière linéaire.

Il l’a appelé OKLAB. La forme polaire est OKLCH.

Ce qui rend OKLCH spécial, ce n’est pas la nouveauté — c’est l’adéquation à l’usage. Trois propriétés ensemble :

  1. La clarté perceptuelle dans OKLCH est véritablement perceptuelle. Un jaune pur à L: 0.7 et un bleu pur à L: 0.7 paraissent avoir la même luminosité sur un écran calibré. Les états de survol définis comme L + 0.05 produisent des décalages visuellement équivalents sur toute la palette.
  2. La teinte est préservée lors des changements de chroma. Si vous réduisez la valeur C de oklch(0.7 0.2 30) à oklch(0.7 0.1 30), la teinte reste en place. En LCH, la même opération introduit souvent un décalage de teinte visible. En OKLCH, vous pouvez aplatir le chroma pour construire une variante atténuée d’une couleur de marque sans qu’elle dérive accidentellement vers une autre teinte.
  3. Les mathématiques sont bon marché. Deux multiplications matricielles et une racine cubique. Implémentable dans une fonction JavaScript de 30 lignes. Pas de tables de correspondance, pas de calibration par appareil, pas de souci de licence.

C’est cette combinaison qui a rendu OKLCH utilisable dans du vrai CSS. Le W3C a ajouté oklch() à CSS Color 4 en 2022. Chrome 111 l’a livré en 2023. Chaque navigateur evergreen le prenait en charge à la mi-2024. Tailwind v4 en a fait le format de palette par défaut la même année.

Les mathématiques : une conversion HEX → OKLCH détaillée

Parcourons #3b82f6 — le blue-500 de Tailwind — jusqu’à OKLCH. Ce sont les mêmes mathématiques que l’outil Convertisseur de couleurs et la spécialisation HEX vers OKLCH exécutent à chaque frappe. Savoir ce qui se passe sous le capot donnera plus de sens aux pièges de la section suivante.

Étape 1 : Hex vers sRGB. Découpez le hex à six chiffres en trois paires et divisez chacune par 255.

const r = 0x3b / 255; // 0.231
const g = 0x82 / 255; // 0.510
const b = 0xf6 / 255; // 0.965

Ce sont des valeurs sRGB encodées en gamma : les valeurs de canal telles que votre fichier image les stocke, avec une courbe non linéaire intégrée pour compenser la manière dont les écrans émettent la lumière.

Étape 2 : sRGB vers linear sRGB. Retirez la courbe gamma pour obtenir des valeurs de canal en lumière linéaire. La transformation par morceaux standard de CSS Color 4 §11.2 :

const linear = (v) => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
const [lr, lg, lb] = [r, g, b].map(linear);
// lr ≈ 0.045, lg ≈ 0.220, lb ≈ 0.923

Étape 3 : Linear sRGB vers XYZ D65. Une multiplication matricielle standard, définie dans CSS Color 4 §15.1 :

const x = 0.4124564 * lr + 0.3575761 * lg + 0.1804375 * lb;
const y = 0.2126729 * lr + 0.7151522 * lg + 0.0721750 * lb;
const z = 0.0193339 * lr + 0.1191920 * lg + 0.9503041 * lb;
// x ≈ 0.265, y ≈ 0.231, z ≈ 0.927

C’est la représentation tristimulus XYZ canonique — la forme « quelles longueurs d’onde est cette couleur, en termes de réponse des cônes humains ».

Étape 4 : XYZ vers LMS. La première matrice d’Ottosson projette XYZ vers un espace de cônes-fondamentaux long/moyen/court ajusté pour OKLAB :

const lms = [
  0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z,
  0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z,
  0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z,
];

Étape 5 : Racine cubique des valeurs LMS. C’est l’étape de compression perceptuelle — analogue à la racine cubique dans CIE LAB :

const lms_ = lms.map(Math.cbrt);

Étape 6 : LMS’ vers OKLAB. La seconde matrice d’Ottosson :

const L = 0.2104542553 * lms_[0] + 0.7936177850 * lms_[1] - 0.0040720468 * lms_[2];
const a = 1.9779984951 * lms_[0] - 2.4285922050 * lms_[1] + 0.4505937099 * lms_[2];
const b_ = 0.0259040371 * lms_[0] + 0.7827717662 * lms_[1] - 0.8086757660 * lms_[2];
// L ≈ 0.629, a ≈ -0.022, b_ ≈ -0.191

Voilà OKLAB. Le canal de clarté perceptuelle L ≈ 0.629 est ce que l’œil perçoit comme « 60 % de luminosité » pour ce bleu particulier.

Étape 7 : OKLAB vers OKLCH (cartésien vers polaire).

const C = Math.sqrt(a * a + b_ * b_);           // 0.193
const H = (Math.atan2(b_, a) * 180 / Math.PI + 360) % 360; // 263.4
// → oklch(0.629 0.193 263.4)

Donc #3b82f6 est oklch(0.629 0.193 263.4). Clarté perceptuelle 0,629, chroma 0,193, teinte 263,4°.

Si vous construisez une rampe 50–950 à partir de cette couleur en faisant varier uniquement L (de 0,95 à 0,15 en 11 paliers), en gardant C et H fixes, vous obtenez une palette où chaque ton est visiblement la même teinte, s’estompant uniformément en clarté perceptuelle. Faites la même chose en HSL et les teintes foncées virent au violet pendant que les teintes claires virent au gris. C’est ça le gain.

Display P3 + Rec2020 : pourquoi OKLCH débloque le gamut chromatique large

OKLCH est non borné. Contrairement à HSL (plafonné à sRGB) et même à LCH (calibré pour les surfaces réfléchissantes), OKLCH n’a pas de gamut chromatique implicite. Vous pouvez écrire oklch(0.7 0.25 30) et produire un rouge vif qui se trouve à l’intérieur du volume colorimétrique de Display P3 mais en dehors de celui de sRGB. Sur un iPhone ou un MacBook récent, ça s’affiche. Sur un écran plus ancien, le navigateur le ramène automatiquement à la représentation sRGB la plus proche.

Cela compte parce qu’Apple, Samsung et le W3C ont tous passé la fin des années 2010 à livrer du matériel à gamut chromatique large. Le MacBook Pro 14” / 16” avec mini-LED livre P3 par défaut. L’iPhone 15 Pro restitue Display P3 dans Safari. Les fleurons Android livrent des dalles Rec2020. En 2025, une fraction substantielle du trafic des systèmes de design est sur du matériel à gamut chromatique large qui peut afficher des couleurs que HSL/sRGB ne peuvent simplement pas exprimer.

OKLCH vous laisse écrire ces couleurs sans ajouter une déclaration @media (color-gamut: p3) séparée. Le navigateur gère le repli. Votre système de design obtient un « utilise le rouge le plus lumineux que l’appareil peut restituer » prêt à l’emploi.

C’est aussi pourquoi OKLCH est le bon format pour les jetons de design. Une variable --brand en OKLCH est une description indépendante de l’appareil de l’intention. Le navigateur détermine quoi restituer sur l’écran dont dispose l’utilisateur, et votre code est portable entre CSS, SwiftUI (qui prend nativement en charge Display P3 Color), Android Compose (conscient de Rec2020) et Flutter.

Tailwind v4 et la révolution des jetons de design

Tailwind v4 — sorti en 2024 — a été le point d’inflexion qui a fait passer OKLCH de la recherche au standard de l’industrie. Les auteurs de Tailwind ont fait trois choix tranchés :

  1. La palette par défaut est OKLCH. Slate, gray, zinc, neutral, stone — chaque couleur Tailwind est définie en oklch() dans le source. Les rampes 50–950 sont uniformes en clarté perceptuelle par construction.
  2. Les thèmes personnalisés utilisent des blocs @theme avec des littéraux OKLCH. Les couleurs de marque sont définies comme jetons oklch() ; les utilitaires en aval (bg-brand-500, text-brand-300) sont générés.
  3. Aucune cérémonie de repli requise. Les navigateurs sans prise en charge OKLCH sont sous la base documentée de Tailwind v4.

Cette dernière décision est celle qui a fait de l’adoption un choix sans piège. Aussi récemment qu’en 2023, les designers devaient livrer à la fois des versions oklch() et hsl() de chaque couleur pour que les anciennes versions de Safari ne cassent pas. Avec Tailwind v4, la base ce sont les navigateurs de 2023 ou plus récents, et OKLCH fonctionne partout.

Le générateur de thème de shadcn/ui suit le même schéma : entrez votre couleur de marque, obtenez une rampe OKLCH en sortie. Le système de design de Vercel utilise OKLCH pour ses couleurs sémantiques. Les échelles de couleurs de Radix Themes sont définies en OKLCH. La communauté a convergé.

Migration pratique : palette HEX → palette OKLCH

Si vous avez aujourd’hui une palette basée sur hex, la migration est mécanique. Voici la recette :

1. Décidez de la structure de votre rampe. Le 50–950 de Tailwind (11 paliers) est le standard de facto et mérite d’être suivi sauf raison spécifique de faire autrement. Des paliers à L = 0,97, 0,93, 0,86, 0,76, 0,63, 0,50, 0,42, 0,34, 0,26, 0,18, 0,10 donnent une rampe perceptuelle fluide.

2. Convertissez votre hex de marque en OKLCH. Utilisez l’outil Convertisseur de couleurs ou HEX vers OKLCH. Vous obtiendrez un triplet comme oklch(0.629 0.193 263.4). Notez la valeur H — c’est votre teinte de marque.

3. Gardez C et H constants ; faites varier L. Construisez la rampe en émettant :

--brand-50:  oklch(0.97 0.193 263.4);
--brand-100: oklch(0.93 0.193 263.4);
...
--brand-950: oklch(0.10 0.193 263.4);

4. Ajustez les paliers extrêmes. Aux L très basses (≤ 0,20) et très hautes (≥ 0,95), les valeurs de chroma élevées tombent hors gamut sRGB. Réduisez C pour ces paliers, ou acceptez le ramènement automatique du navigateur. Les valeurs par défaut de Tailwind réduisent le chroma vers les deux extrémités — copiez ce schéma.

5. Définissez des alias sémantiques. --surface: var(--brand-50), --surface-elevated: var(--brand-100), --text-primary: var(--brand-900), etc. Maintenant vos jetons de design se lisent comme une intention, pas comme des couleurs.

6. Vérifiez le contraste. Utilisez APCA Lc ou les ratios de contraste WCAG 2 pour confirmer que chaque paire --text-* contre chaque paire --surface-* atteint votre seuil d’accessibilité. Comme le L d’OKLCH est perceptuel, les mathématiques de contraste sont plus fiables qu’elles ne le seraient en HSL.

Une équipe qui mène cette migration sur une palette héritée de 60 couleurs aboutit typiquement à une palette OKLCH plus petite et plus uniforme de 30–40 jetons en un seul après-midi. La nouvelle palette se livre plus petite, génère un CSS plus petit, et produit un mouvement tonal visiblement meilleur dans les états de survol et désactivés sans aucun réglage supplémentaire.

Pièges et comment les gérer

Quelques points à savoir avant de se lancer :

Avertissements hors gamut. Certaines valeurs OKLCH tombent en dehors de Display P3 ou de sRGB. Les navigateurs modernes gèrent automatiquement le ramènement à la couleur valide la plus proche, mais ce ramènement est avec perte : votre oklch(0.7 0.25 30) peut s’afficher légèrement moins saturé que ce que vous avez écrit. Des outils comme la ligne de gamut chromatique du Convertisseur de couleurs vous indiquent si votre couleur est sRGB-safe, P3-safe ou Rec2020-safe, et proposent un Snap-to-sRGB en un clic pour que ce que vous écrivez soit ce que vous voyez.

Bizarreries de chroma sous-pixel. Le chroma OKLCH est non borné, mais la plage utile est à peu près de 0 à ~0,4 pour les couleurs visibles. Les valeurs au-dessus de 0,4 ne sont atteignables qu’avec de la lumière laser monochromatique — ce ne sont pas des couleurs physiques qu’un écran peut restituer. Le Convertisseur de couleurs plafonne le curseur de chroma à 0,4 pour cette raison ; les valeurs au-delà ne produisent aucune différence perceptible sur aucun écran réel.

Compatibilité navigateur, style 2025. Chrome 111+, Safari 15.4+, Firefox 113+ prennent tous en charge oklch() nativement. Les navigateurs pré-2023 ne le font pas. Si vous devez prendre en charge IE/Edge hérités ou Safari mobile plus ancien (1–3 % du trafic selon votre audience), vous pouvez coupler une déclaration OKLCH avec un repli hex en utilisant @supports (color: oklch(0 0 0)) — mais pour les jetons de système de design livrés en 2025, le coût du repli l’emporte souvent sur le bénéfice hérité.

La permanence du code hex. OKLCH sert pour l’intention du système de design. Votre CMS peut encore avoir besoin d’une valeur hex pour des raisons héritées (signatures e-mail, documents Office, listes de contrôle d’assets de marque). Gardez une table de correspondance générée qui émet le hex ramené à sRGB pour chaque jeton OKLCH, mais ne créez pas en hex.

Ne confondez pas OKLCH et OKLAB. OKLAB est la forme rectangulaire (canaux L, a, b) ; OKLCH est le même espace colorimétrique sous forme polaire (L, C, H). Vous passez de l’un à l’autre avec une étape cartésien↔polaire. Utilisez OKLCH pour les jetons (plus lisible, plus facile à rampe) ; utilisez OKLAB en interne si vous avez besoin d’interpoler ou de mélanger des couleurs.

Essayez-le sur votre propre palette

La façon la plus rapide de voir en action ce que nous venons de décrire est de déposer un hex de marque dans le Convertisseur de couleurs. Tapez votre couleur de marque dans le champ HEX et lisez la sortie OKLCH. Puis déplacez les curseurs côté OKLCH et observez comment la même teinte reste la même teinte quand vous réduisez le chroma, et comment la même clarté perceptuelle reste la même clarté perceptuelle quand vous faites pivoter la teinte. Après quelques minutes, vous aurez un sens intuitif de pourquoi HSL a toujours été le mauvais outil pour les rampes tonales, et de pourquoi chaque système de design sérieux est passé à autre chose.

Pour la conversion HEX vers OKLCH spécifique, utilisez HEX vers OKLCH — les mêmes mathématiques que cet article, avec un bénéfice supplémentaire : il vous indique la classification de gamut chromatique (sRGB / Display P3 / Rec2020) pour que vous sachiez lesquelles de vos couleurs de marque sont sûres partout et lesquelles ont besoin d’un appareil à gamut chromatique large pour s’afficher pleinement.

Voilà OKLCH. Ça vaut la migration. Bien fait, vous n’écrirez plus jamais hsl().

Articles connexes

Voir tous les articles