Mes dégradés sont plus beaux que les vôtres
☠️ La Zone Grise de la Mort
Dans les années 2010, nous avons eu le plaisir de découvrir les fonctions linear-gradient
et radial-gradient
qui permettent de définir des dégradés directement en CSS.
Je me souviens d'avoir expérimenté diverses combinaisons, avec des résultats assez inégaux. En utilisant certaines couleurs, je voyais une bande grisâtre qui ne semblait pas coller avec l'effet recherché.
Si je vous dis que l'on mélange du bleu et du jaune, on obtient du vert, pas vrai ?
🟡 + 🔵 = 🟢
Apparemment pas dans un navigateur, avec CSS.
background: linear-gradient(to right, #FFFF00, #0000FF);
J'ai bien tenté de refaire l'expérience avec un outil tel qu'Illustrator et j'ai été surpris d'obtenir un résultat similaire. A l'époque, cela m'avait semblé être une curiosité, un peu décevante mais assez négligeable pour être rapidement oubliée. Elle l'est restée jusqu'à ce que je découvre en 2022 ce tweet de Fin Mourhouse :
Dans ce fil, le chercheur nous explique les origines mathématiques de ce phénomène, parfois appelé la Zone Grise de la Mort ☠️
En RVB, on sait comment définir une couleur grise : il suffit de déclarer le même niveau de rouge, de vert et de bleu.
Si le niveau de chaque couleur est élevé, on s'approchera du blanc et inversement, s'il est bas, notre gris tendra vers le noir.
En représentant le RVB sous la forme d'un cube, on peut visualiser "la diagonale du gris" qui le traverse : de l'arrière-plan vers l'avant et du bas vers le haut
Source : Wikimedia
Maintenant, pensons à la manière dont notre dégradé est construit.
À chaque pixel situé sur le parcours du dégradé, le navigateur va afficher une couleur qui sera calculée en fonction des valeurs de départ et des valeurs d'arrivée. Il va faire ce calcul pour le rouge, le vert et le bleu : il augmentera ou diminuera régulièrement leur valeur de départ, jusqu'à leur valeur d'arrivée.
Dans notre exemple du jaune et du bleu, nos points de départ et d'arrivée sont soit 0, soit 255. À mi-chemin, il y a un point où toutes les valeurs sont égales. À cet endroit, la synthèse RVB est nécessairement grise :
Notre méthode de calcul du dégradé est facile à comprendre, mais elle nous pousse vers la diagonale du gris et les couleurs semblent plus ternes, moins lumineuses à mesure que nous nous en approchons.
Dans la suite de l'article, nous allons chercher à obtenir un meilleur dégradé, tout en privilégiant une méthode de calcul simple et si possible intuitive.
Voici les problèmes à résoudre :
éviter la zone grise de la mort
préserver la luminosité des couleurs
🌈 HSL : oui... Mais non 🌧️
Intuitivement, on peut penser que l'écriture HSL (ou le HWB qui est très proche) est un bon moyen d'éviter la zone grise de la mort.
HSL signifie Hue, Saturation, Lightness (en français : Teinte, Saturation, Lumière). Ce système représente les couleurs sur un disque :
les teintes (H) sont réparties de 0 à 360°.
la saturation (S) de la teinte est exprimée par le rayon du disque de 0 (la couleur totalement desaturée, donc grise) à 100 (la saturation est maximale).
On y ajoute une troisième dimension :
- la lumière (L) qui va de 0 (aucune luminosité, la couleur est noire) à 100 (luminosité maximale, la couleur est blanche).
On peut alors imaginer un dégradé qui ferait varier uniquement la teinte, tout en gardant une saturation et une luminosité constante. Voici la visualisation d'un déplacement sur le disque qui évite la zone grise :
Source : Wikimedia
Cela semble une bonne piste. Mais attention, déclarer nos couleurs avec hsl()
au lieu de rgb()
ne va pas suffire. hsl()
est un "sucre syntaxique" : la valeur déclarée sera systématiquement convertie en RVB et nous obtiendrons le même résultat qu'avant.
Ce qui nous intéresse, ce n'est pas comment on déclare une couleur, mais comment on calcule notre dégradé.
C'est là qu'intervient la spécification CSS Color Module Level 4 qui introduit la fonction color-interpolation-method
qui nous permet de spécifier un mode de calcul.
Cela nous donne :
background: linear-gradient(to right in hsl, #0000ff, #ffff00);
⚠️ A ce jour (mars 2024) la fonction
color-interpolation-method
pour les dégradés n'est pas disponible dans tous les navigateurs.
On voit que nous n'avons même pas besoin de déclarer nos couleurs en HSL, ce qui importe c'est la déclaration in hsl
.
Grâce à cette méthode, nous évitons la zone grise et nous obtenons enfin notre couleur verte entre le bleu et le jaune.
Mais le résultat est un peu déroutant. En allant vers le bleu, on passe par une zone turquoise qui parait franchement plus lumineuse que l'ensemble du dégradé. Ça ne semble pas très naturel.
💡 sRGB : des problèmes de luminosité
Quand nous utilisons les méthodes rgb()
, hsl()
, hwb()
, nous définissons des couleurs contenues dans l'espace colorimétrique sRGB (Le "s" signifie standard). Cet espace sert à déclarer les couleurs disponibles sur la plupart des écrans que nous utilisons. Il est très pratique, car il permet d'en définir un très grand nombre en combinant seulement 3 signaux lumineux. Mais il a aussi ses défauts.
Les couleurs de cet espace ont été définies pour nos yeux, mais elles sont aussi contraintes par la technologie, par la conception même des écrans.
Il en va que le sRGB contient des ajustements spécifiques par rapport à la luminosité réelle : la correction Gamma.
Mais aussi que les valeurs maximales de rouge, vert et bleu ne sont pas égales en termes de luminosité perçue par nos yeux (on parlera alors de "luminance").
Ces deux points ont un impact important sur les calculs que nous voulons effectuer pour créer notre dégradé.
La correction Gamma 🕶️
Pour des raisons pratiques la répartition de la luminosité dans le modèle RVB est artificiellement corrigée par rapport à la luminosité naturelle.
Nous percevons mieux les nuances sombres que claires. Or, si l'on découpait linéairement la lumière selon sa luminosité, nous disposerions de beaucoup plus de nuances claires et pas assez de nuances sombres.
Voici un schéma avec à gauche le découpage corrigé et à droite un découpage linéaire :
Dans la version corrigée, la répartition des nuances sombres nous semble plus adaptée à nos besoins. Dans le découpage linéaire, la quantité de nuances claires paraît trop importante.
La correction est de plus en plus importante au fur et à mesure que la luminosité augmente. Cette non-linéarité implique des calculs plus compliqués pour réaliser un dégradé avec une luminosité constante. Il serait plus efficace d'effectuer nos calculs à partir des valeurs du RVB linéaire.
Nous retrouvons alors notre fonction color-interpolation-method
. Elle nous permet de spécifier des espaces de couleur différents dans nos dégradés :
background:linear-gradient(to right in srgb, #FFFF00, #0000FF);
background:linear-gradient(to right in srgb-linear, #FFFF00, #0000FF);
Le premier dégradé utilise l'espace RVB "classique" (sRGB), le second utilise le RVB linéaire.
La différence de luminosité entre les deux méthodes de calcul est flagrante : sombre pour la première, plus lumineuse pour la seconde... Mais dans les deux cas subsiste notre premier problème : nous passons par le gris.
La luminosité perçue 👁️
Deuxième problème de notre espace sRGB : les valeurs maximales n'ont pas une luminosité équivalente, en tout cas dans notre perception. C'est flagrant si l'on compare le vert et le bleu :
La conversion en niveaux de gris rend cette différence encore plus évidente. Ces différences de luminosité ne sont pas prises en compte sur le disque chromatique que nous utilisons avec HSL.
Le modèle HSL est destiné à l'espace sRGB, et donc les valeurs de luminosité et de saturation sont harmonisées afin de créer un disque chromatique cohérent avec cet espace.
Ce schéma représente horizontalement les teintes (Hue) et verticalement la luminosité (Lightness) :
Source : Wikimedia
Si l'on se place à 50% de luminosité (axe vertical L) et que l'on parcourt les différentes couleurs, la luminosité est loin de nous paraître constante.
Ceci explique le manque de consistance dans notre dégradé HSL.
À ce stade, on se dit que prendre en compte l'ensemble des caractéristiques du sRGB et les intégrer dans nos calculs de dégradé risque d'être un peu compliqué. En s'appliquant, on doit pouvoir y arriver, mais ce n'est pas aussi intuitif qu'on le voudrait et difficilement automatisable.
Mais alors que faire ?
Prenons un peu de recul et revenons sur cette histoire d'espace colorimétrique.
👩🚀 L'espace colorimétrique sRGB
Nous travaillons en sRGB car c'est ce dont les écrans ont besoin et historiquement nous avons les outils CSS pour transmettre cette information dans cet espace de couleur. Mais cet espace ne contient pas toutes les couleurs possibles, il est restreint. Cet ensemble de couleurs, parfois appelé gamut, peut être élargi. Certains appareils vont proposer des gamut couvrant une plage de couleur plus importante. C'est le cas de certains Macbook qui utilisent le gamut P3 ou d'écrans de télé HD qui utilisent rec2020.
Le schéma CIE 1931 ci-dessous propose une représentation mathématique des couleurs visibles. Sur cette version, on a indiqué les plages des différents gamuts :
Source : Wikimedia
Au milieu, notre sRGB est un triangle, avec ses trois sommets : rouge, vert et bleu. Chacune de ces valeurs est envoyée à l'écran et sera affichée par un pixel donné, ce qui produira pour nos yeux la couleur finale.
Pour explorer cet espace, nous utilisons rgb() et hsl(). Mais comme on le disait, certains appareils nous permettent d'accéder à d'autres espaces colorimétriques. Alors comment y définir des couleurs ?
👽 Plus d'espace : plus de modèle !
C'est là qu'intervient de nouveau le CSS Color Module Level 4. Cette spécification nous met à disposition des méthodes permettant de définir des couleurs dans et hors du sRGB. Ces nouveaux modèles ne sont pas contraints par un gamut spécifique et ne sont pas conçus comme le sRGB.
Nous allons nous intéresser à :
lab()
oklab()
lch()
oklch()
Ces fonctions permettent de déclarer une couleur dans l'espace colorimétrique Lab*. Cet espace couvre toutes les couleurs visibles par nos yeux et il a été créé pour prendre en compte notre perception, en particulier la différence de luminosité que nous percevons en fonction des teintes.
La fonction lab() permet de déclarer une couleur selon sa luminosité (l) et sa teinte selon deux axes (a et b) qui vont respectivement du vert au rouge et du bleu au jaune.
lch() permet d'exploiter cet espace mais sous une forme cylindrique (comme le HSL).
Les versions "ok" (oklab et oklch) corrigent certains défauts du lab (notamment dans la luminosité des bleus).
Avec ce modèle, nous contrôlons :
la luminosité (l)
le chroma (c) qui indique la quantité de couleur et que l'on peut rapprocher de la saturation
la teinte (h) qui désigne une couleur sur un cercle
Nous allons donc refaire notre dégradé en okLCH qui semble être le candidat parfait :
Interpolation circulaire
Prise en compte de la luminosité perçue
🍭 Bon ! Et notre dégradé alors ?
background:linear-gradient(to right in okLCH, #ffff00, #0000ff)
Et enfin ! On peut dire que ce dégradé correspond à l'idée que nous en étions faite. Ça été un peu plus long que prévu, mais cela nous a permis de comprendre l’intérêt des différents modèles à notre disposition.
Je ne vais pas rentrer plus en avant sur les qualités de okLCH, cela me demanderait beaucoup de travail et cet article serait interminable. Et surtout cela a déjà été très bien fait par l'agence Evil Martians, dont je ne saurais trop vous conseiller l'excellent article :
Ainsi que leur color picker qui permet de convertir des couleurs en okLCH et aussi de visualiser nos couleurs dans les limites de différents gamuts :
🎨 okLCH au service du Design System
Au-delà des dégradés, ce qui est intéressant, c'est la consistance d'un modèle comme okLCH pour systématiser nos déclinaisons de couleur. Générer une palette de couleur cohérente à partir d'un ensemble de teintes sera plus simple, notamment si l'on fait usage de la fonction CSS color-mix()
.
Pour découvrir ce sujet, je vous conseille la vidéo de Kevin Powell sur la fonction CSS color-mix()
qui nous explique comment décliner ses couleurs et comment les méthodes d'interpolation vont influencer les résultats.
🛠️ Et en prod, ça marche ?
color-interpolation-method
1. dans les dégradés
Rappelons-le, color-interpolation-method
, n'est pas disponible partout pour les dégradés (notamment dans Firefox). Si vous l'utilisez, vous devrez employer un fallback.
Il est aussi possible d'utiliser un générateur de dégradé qui simule des interpolations spécifiques, par exemple :
2. dans color-mix()
L'usage d'une méthode d'interpolation avec color-mix() est supporté par les navigateurs récents
oklch()
Il est possible de déclarer vos couleurs directement en utilisant oklch()
. La compatibilité semble plutôt bonne.
Conclusion
En partant de cette curiosité graphique qu'est "la zone grise de la mort", je ne pensais pas découvrir autant de problématiques liées à la couleur. En tant que dev front, la couleur peut sembler être un sujet annexe qui concerne essentiellement le design. Mais en creusant, on comprend très vite que l'usage de la couleur sur nos écrans est très lié à la technologie et aux évolutions techniques. Comprendre cette histoire et ces contraintes, c'est aussi mieux comprendre et exploiter les outils à notre disposition.
Les fonctionnalités récentes du CSS en matière de couleur peuvent largement impacter le travail des design.ers.euses et leur ouvrir de nouvelles perspectives créatives.
Il est donc important de réfléchir ce sujet ensemble, à ce stade les possibilités offertes par CSS et les navigateurs semblent plus en avance que celles offertes par les outils de design. Ainsi, dans Figma, il n'est pas possible d'utiliser nativement des modèles "exotiques" comme okLCH ou d'utiliser des méthodes d'interpolation autre que le RVB pour les dégradés. Il existe cependant des plugins, reste à voir s'ils sont efficaces.
Bibliographie
Gamut / Espaces colorimétriques
Dégradés
Easy CSS Gradient Generator Tool (avoids gray dead zones ☠️)
Perceptual gradients - Share an idea - Figma Community Forum
Gamma
Luminosité perçue
Models
OKLCH in CSS: why we moved from RGB and HSL—Martian Chronicles, Evil Martians’ team blog
The CIELAB Lab* System – the Method to Quantify Colors of Coatings - Prospector Knowledge Center
Visual representation of color names in different color models
hue-interpolation-method - CSS: Cascading Style Sheets | MDN
Color-mix
Create a color theme with CSS Relative Color Syntax, CSS color-mix(), and CSS color-contrast() –Bram.us
Illustration :Susan Wilkinson
Subscribe to my newsletter
Read articles from Vincent Le Hen directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by