Aller au contenu
Allin

Dégradés, banding, et pourquoi le milieu paraît terne

Publié le 21/08/2025 · 18 min de lecture · Outils pour développeurs

Daniel Okonkwo

Daniel OkonkwoDéveloppeur front-end et rédacteur Tech chez Allin

Performance web · Formats de fichiers

Vérifié à partir de 6 sources

Voir le profil
En bref

Un dégradé CSS interpole les nombres stockés dans les couleurs, et ces nombres sont encodés en gamma. Le sRGB dépense ses 8 bits là où l'œil est sensible plutôt qu'en proportion de la lumière : moyenner deux valeurs encodées ne donne donc pas la couleur ayant la quantité de lumière moyenne. Prends le rouge #ff0000 vers le vert #00ff00. Le milieu sRGB brut vaut #808000, de luminance relative WCAG 0,1986. Linéarise les deux extrémités, moyenne-les là, ré-encode : tu obtiens #bcbc00, de luminance 0,4639 — exactement la moyenne des luminances des extrémités, comme doit l'être une mi-lumière. Le milieu par défaut est 57,2 % en dessous, et ce déficit est la bande sombre et terne que l'on voit au centre de tout dégradé à deux couleurs. Un second problème, sans rapport, est le banding. Huit bits par canal donnent 256 valeurs : une rampe entre deux couleurs sombres voisines peut ne contenir que 28 couleurs distinctes ; étirée sur 1920 pixels, cela fait une bande large de 68,6 pixels en moyenne, visible d'un bout à l'autre de la pièce. Le tramage la masque en échangeant du bruit spatial contre de la résolution tonale. CSS permet désormais de corriger le premier problème directement avec `in oklab` ou `in oklch`, et le mot-clé de direction de teinte décide du sens de parcours de la roue.

Interpoler en sRGB revient à moyenner des nombres encodés en gamma : le milieu de rouge à vert vaut #808000 alors que la réponse à mi-lumière est #bcbc00 — 57,2 % de lumière en moins. Le banding est un autre problème d'arithmétique : 8 bits donnent 256 paliers, et un dégradé sombre peut n'en avoir que 28. Voici les deux, calculés.

La moyenne est prise dans les mauvaises unités

Un linear-gradient en CSS fait la chose évidente : pour chaque pixel il calcule une position t entre 0 et 1 et renvoie les couleurs extrêmes mélangées selon t. La subtilité est ce qu'il mélange. Un code hexadécimal n'est pas une mesure de lumière ; c'est un mot de code, et le sRGB assigne délibérément ces mots de façon non linéaire pour dépenser les 256 valeurs disponibles là où la vision humaine sait les distinguer. La valeur stockée est à peu près la lumière élevée à la puissance 1/2,2, et non la lumière elle-même.

La fonction de transfert sRGB est écrite dans la norme et facile à implémenter. Pour décoder, divise la valeur 8 bits par 255 pour obtenir c, puis renvoie c ÷ 12,92 si c vaut au plus 0,04045, et ((c + 0,055) ÷ 1,055) à la puissance 2,4 sinon. Pour encoder, inverse : multiplie par 12,92 en dessous de 0,0031308, sinon 1,055 fois la valeur à la puissance 1/2,4, moins 0,055. Ces deux fonctions suffisent à passer des mots de code à la lumière et retour.

Passe-y un seul nombre et l'ampleur du problème saute déjà aux yeux. La moitié de la lumière — linéaire 0,5 — s'encode en 0,735357, soit 188 en 8 bits, ou #bcbcbc. La couleur que la plupart appellent gris cinquante pour cent, #808080, se décode en 0,215861. Ce n'est la moitié de rien. C'est le mot de code situé à mi-chemin du code, ce qui n'a rien à voir avec le milieu de la lumière.

Du rouge au vert, mesuré des deux façons

Prends #ff0000 vers #00ff00 et calcule le milieu deux fois. La façon naïve moyenne les valeurs stockées : (255 + 0) ÷ 2 = 127,5 pour le rouge, idem pour le vert, 0 pour le bleu. Cela donne #808000, et le passer dans le calcul de luminance WCAG donne 0,1986. La bonne façon décode d'abord : le rouge devient linéaire (1, 0, 0) et le vert (0, 1, 0), la moyenne vaut (0,5, 0,5, 0), et le ré-encodage donne 187,5 sur les deux canaux — #bcbc00, de luminance 0,4639.

Le second nombre est vérifiable indépendamment : la luminance relative de #ff0000 vaut 0,2126 et celle de #00ff00 vaut 0,7152, et leur moyenne est 0,4639 — exactement ce que mesure le milieu linéaire. C'est la définition du mi-chemin en lumière, et la méthode linéaire la reproduit au dernier chiffre. Le milieu sRGB livre 0,1986, soit 57,2 % en dessous. Bien plus de la moitié de la lumière qui devrait se trouver au centre du dégradé manque à l'appel.

Le même déficit apparaît partout où les deux extrémités sont éloignées dans l'espace des codes. Noir vers blanc : milieu sRGB #808080 à la luminance 0,2140, milieu linéaire #bcbcbc à 0,5000, le même déficit de 57,2 %. Blanc vers bleu : #8080ff à 0,2708 contre #bcbcff à 0,5361, un déficit de 49,5 %. Ce n'est pas une bizarrerie de couleurs particulières ; c'est la forme de la fonction de transfert, et cela vaut pour tout dégradé par défaut sur le web.

Du bleu au jaune, ça passe par le gris — autre défaut, autre remède

Il est tentant d'imputer aussi au problème de gamma le centre gris d'un dégradé bleu-jaune, et cela s'avère faux. Calcule les deux milieux pour #0000ff vers #ffff00. Le milieu sRGB est #808080, de luminance 0,2140. Le milieu en lumière linéaire est #bcbcbc, de luminance 0,5000. La version linéaire est bien plus claire, exactement comme le prévoit la section précédente — et elle reste un gris parfaitement neutre. Mesurés en OKLCH, tous deux ont un chroma de 0,000. Travailler en lumière linéaire a corrigé l'obscurité et n'a strictement rien fait contre la grisaille.

La raison est que le bleu et le jaune sont presque opposés dans le cube RVB : moyenner leurs canaux annule la couleur quelles que soient les unités utilisées. Toute interpolation rectangulaire — sRGB, sRGB linéaire, et même OKLab, qui est rectangulaire bien que perceptuel — parcourt une droite passant par le milieu de l'espace des couleurs, et le milieu de cet espace est neutre. Seule une interpolation polaire, qui transporte le chroma et fait tourner la teinte au lieu de moyenner des canaux opposés, contourne par l'extérieur.

Une démonstration plus nette utilise une paire qui reste dans le gamut sRGB : #007bff vers #ff6b00, un bleu et un orange dont les teintes OKLCH valent 257,2° et 45,1°. En sRGB brut, le milieu est #807380, un gris-mauve terne de chroma OKLCH 0,024 — la couleur est morte. En sRGB linéaire, c'est #bc73bc, chroma 0,131. En OKLab, #a285a5, chroma 0,058. En OKLCH par l'arc de teinte court, c'est #d454c9, chroma 0,208 : un magenta vif, et le chroma vaut désormais à peu près la moyenne de ceux des extrémités plutôt qu'une fraction. Deux mêmes arrêts, cinq centres différents.

Le banding : 256 valeurs étirées sur 2560 pixels

Le banding n'a rien à voir avec l'espace d'interpolation. C'est un problème de dénombrement. Un canal 8 bits a 256 valeurs possibles : un dégradé ne peut jamais contenir plus de couleurs distinctes que n'en couvrent ses extrémités. Si une rampe est tracée sur plus de pixels qu'elle n'a de valeurs distinctes, certaines valeurs se répètent forcément, et chaque série répétée est une bande plate.

Rastériser les rampes pixel par pixel et compter donne les vrais chiffres. Un dégradé complet du noir au blanc contient 256 couleurs distinctes quelle que soit sa largeur. Sur 320 pixels, la bande moyenne fait 1,3 pixel et la plus longue série plate 2 pixels : personne ne les verra. Sur 1920 pixels, la moyenne monte à 7,5 pixels et la plus longue série à 8. Sur 2560 pixels, c'est 10,0 et 11 — une rayure visible, sur la plus large plage tonale possible.

Les vrais dégradés sont bien pires, car les designers couvrent rarement toute la plage. Un hero sombre à la mode allant de #1a1a2e à #16213e ne contient que 28 couleurs distinctes. Sur 1920 pixels, cela fait une bande moyenne de 68,6 pixels, la plus longue série plate atteignant 120 pixels. Sur 2560 pixels, la moyenne est de 91,4 pixels. Une surface blanc sur blanc discrète, de #ffffff à #f5f5f5, contient 11 couleurs : à 1920 pixels, la bande moyenne fait 174,5 pixels de large. Ce ne sont pas des artefacts qu'un visiteur pourrait remarquer ; ce sont des rayures plus larges qu'un bouton.

Une règle utilisable en découle. Si l'on prend 3 pixels comme largeur à partir de laquelle une bande plate se lit comme une arête plutôt que comme un ton continu, le banding devient visible dès que la largeur tracée dépasse trois fois le nombre de niveaux distincts. Une rampe complète de 256 niveaux tient jusqu'à 768 pixels. Une rampe de 52 niveaux tient jusqu'à 156. Le hero à 28 niveaux tient jusqu'à 84 pixels, et la surface à 11 niveaux jusqu'à 33. Presque tout dégradé décoratif d'une page moderne est tracé bien au-delà de sa limite.

Le tramage, et ce qu'apporteraient plus de bits

Le tramage est le remède standard et il fonctionne en échangeant un type d'erreur contre un autre. Au lieu d'arrondir chaque pixel à la valeur disponible la plus proche, on ajoute un peu de bruit avant l'arrondi : les pixels proches d'une frontière se répartissent entre les deux valeurs voisines dans un rapport proportionnel à leur proximité. La moyenne sur toute petite zone est alors correcte même si aucun pixel ne l'est, et l'œil intègre la zone plutôt que le pixel. La rayure visible cède la place à un grain léger.

Sur le web, on contrôle rarement le rastériseur : le tramage est donc appliqué à la main — superpose une texture de bruit en mosaïque, de très faible contraste, sur le dégradé, à une opacité d'un ou deux pour cent, ou génère le dégradé en SVG avec une couche feTurbulence. Les deux approches brisent les séries plates. L'astuce est de garder le bruit sous le seuil où il se lit comme une texture : si tu vois le grain, tu as dépassé la mesure et échangé une rayure contre une surface sale.

Plus de bits supprimeraient totalement le besoin. À 10 bits par canal, il y a 1024 niveaux, et la même rampe pleine plage sur 1920 pixels donne une bande moyenne de 1,88 pixel — sous le seuil de visibilité, donc sans tramage. Les écrans à gamut étendu et les chaînes HDR offrent de plus en plus cette profondeur, et les couleurs CSS écrites en oklch() ou avec la fonction color() savent l'exprimer. La contrainte, c'est toute la chaîne : une valeur 10 bits composée dans un tampon 8 bits redevient une valeur 8 bits.

Les mots-clés CSS : in oklch, in lab, et la direction de teinte

CSS Color 4 permet de nommer l'espace d'interpolation directement dans la fonction de dégradé : linear-gradient(in oklch, #007bff, #ff6b00). Les espaces acceptés incluent srgb, srgb-linear, lab, oklab, lch, oklch, hsl, hwb et xyz. Les rectangulaires — srgb, srgb-linear, lab, oklab, xyz — interpolent les coordonnées composante par composante. Les polaires — hsl, hwb, lch, oklch — interpolent un angle de teinte, et c'est là que se logent les surprises.

Pour un espace polaire, tu peux ajouter une méthode d'interpolation de teinte : shorter, longer, increasing ou decreasing, chacune suivie du mot hue. Shorter prend l'arc d'au plus 180°, valeur par défaut. Longer prend l'arc complémentaire, en passant par le long chemin. Increasing force l'angle à croître, decreasing à décroître. Exemple travaillé : en interpolant de la teinte 20 à la teinte 320, shorter donne un arc de −60° et le chemin 20, 5, 350, 335, 320, tandis que longer donne un arc de +300° et le chemin 20, 95, 170, 245, 320 — par le vert et le bleu, un dégradé entièrement différent à partir des deux mêmes arrêts.

Il y a une deuxième chose à surveiller dans les espaces polaires. OKLCH peut décrire des couleurs hors du gamut sRGB, et le chemin polaire bombe vers l'extérieur : le milieu OKLCH du bleu #0000ff et du jaune #ffff00 se calcule en coordonnées qui donnent un triplet RVB hors plage et doit subir un gamut mapping avant affichage. Les navigateurs s'en chargent, mais le résultat portera moins de chroma que l'arithmétique n'en demandait : un dégradé éclatant dans un sélecteur de couleurs peut arriver nettement plus sage à l'écran. La paire dans le gamut utilisée plus haut évite complètement le problème, d'où la leçon pratique : vérifie le milieu, pas seulement les extrémités.

Le fondu vers la transparence, et l'alpha prémultiplié

Le dernier bogue classique du dégradé, c'est le voile gris à la fin d'un fondu vers la transparence. Il survient quand l'interpolation traite couleur et alpha comme quatre nombres indépendants. Faites un fondu du blanc vers `transparent` : comme le mot-clé CSS `transparent` vaut rgb(0 0 0 / 0) — un noir transparent — le canal de couleur est tiré du blanc vers le noir en même temps que l'alpha diminue. Au milieu, la couleur est un gris à 50 % et l'alpha vaut 0,5 : sur une page blanche, le pixel s'affiche donc en #bfbfbf, une bande sale visible là où il devrait y avoir un fondu propre.

L'alpha prémultiplié supprime l'artefact. Avant d'interpoler, multipliez chaque canal de couleur par son propre alpha ; interpolez ensemble les canaux prémultipliés et l'alpha ; puis redivisez à la fin. Un arrêt totalement transparent ne contribue en rien à la couleur, ses canaux prémultipliés étant tous nuls : il ne peut donc entraîner la couleur visible nulle part. Le même fondu du blanc vers le noir transparent, en prémultiplié, donne #ffffff au milieu sur fond blanc au lieu de #bfbfbf, et #808080 sur fond noir au lieu de #404040.

Les dégradés CSS sont spécifiés pour interpoler en espace prémultiplié, et les navigateurs modernes s'en acquittent correctement. Le correctif manuel, qui mérite d'être connu car il survit dans le code canvas, en SVG, dans les éditeurs d'images et sur les moteurs anciens, consiste à nommer explicitement l'arrêt transparent dans la couleur d'où tu pars : utilise rgb(255 255 255 / 0) plutôt que le mot-clé `transparent`. Calcule les deux milieux et l'équivalence est exacte — un fondu du blanc vers un blanc transparent donne #ffffff sur blanc et #808080 sur noir, par l'une ou l'autre méthode, prémultipliée ou non.

Chroma OKLCH
Le milieu de #007bff vers #ff6b00 dans cinq espaces d'interpolation, calculé
Espace d'interpolationCSSMilieuChroma OKLCHRésultat
sRGB (par défaut)in srgb#8073800,024Sombre et presque incolore
sRGB en lumière linéairein srgb-linear#bc73bc0,131Clarté correcte, teinte éteinte
OKLab (rectangulaire)in oklab#a285a50,058Clarté régulière, encore délavé
OKLCH, arc courtin oklch shorter hue#d454c90,208Magenta vif, chroma préservé
OKLCH, arc longin oklch longer hue#00b1450,208Vert, hors gamut et écrêté
Générateur de dégradé CSSConstruis un dégradé CSS linéaire ou radial à partir de deux couleurs et d'un angle, avec sortie CSS et Tailwind.Essayer l'outil

Questions fréquentes

Pourquoi mon dégradé à deux couleurs a-t-il une bande sombre au milieu ?
Parce que le navigateur a moyenné des nombres encodés en gamma au lieu de la lumière. Le sRGB stocke une valeur proche de la lumière élevée à la puissance 1/2,2 : la moyenne arithmétique de deux valeurs stockées correspond donc à bien moins de lumière que la moyenne des deux lumières. Rouge #ff0000 vers vert #00ff00 le rend concret : le milieu sRGB brut est #808000, de luminance relative 0,1986, alors que le vrai milieu à mi-lumière est #bcbc00 à 0,4639 — exactement la moyenne des luminances des extrémités, 0,2126 et 0,7152. Le comportement par défaut est 57,2 % en dessous de la lumière qu'il devrait porter, et ce déficit est la bande sombre. Le correctif d'une ligne en CSS consiste à nommer un meilleur espace d'interpolation : linear-gradient(in oklab, red, lime) ou linear-gradient(in srgb-linear, red, lime). Les deux relèvent le centre. Si tu veux en plus que le milieu reste coloré au lieu de virer au neutre, il te faut un espace polaire — in oklch — car c'est un problème distinct, décrit à la réponse suivante.
Pourquoi le bleu vers le jaune passe-t-il par le gris même dans un meilleur espace ?
Parce que la grisaille et l'obscurité sont deux défauts distincts. Bleu et jaune sont presque opposés : moyenner leurs canaux annule la couleur quelles que soient les unités. Les nombres le confirment : le milieu sRGB de #0000ff et #ffff00 est #808080, de luminance 0,2140, et le milieu en lumière linéaire est #bcbcbc, de luminance 0,5000. La version linéaire est bien plus claire, comme prévu, et toutes deux ont un chroma OKLCH de 0,000 — parfaitement neutre. Même OKLab, pourtant perceptuel, donne #6cabc7 avec un chroma de seulement 0,077, car OKLab est rectangulaire et une droite entre deux couleurs opposées passe par l'axe neutre. Seule une interpolation polaire l'évite, car elle fait tourner la teinte et transporte le chroma au lieu de moyenner des canaux opposés. Utilise linear-gradient(in oklch, blue, yellow). Sache que pour cette paire précise le milieu polaire tombe hors du gamut sRGB et sera ramené dedans : tu obtiendras moins de chroma que promis. Pour un centre vif à coup sûr, ajoute un arrêt intermédiaire explicite dans la teinte que tu veux vraiment.
Quelle largeur un dégradé peut-il atteindre avant de bander ?
Cela dépend entièrement de l'écart entre les extrémités, pas de la taille de l'élément. Compte les couleurs 8 bits distinctes que la rampe peut contenir, puis multiplie par environ trois : en prenant 3 pixels comme largeur à partir de laquelle une bande plate se lit comme une arête, ce produit est la largeur d'apparition du banding. Une rampe complète du noir au blanc contient 256 couleurs et tient jusqu'à environ 768 pixels. Une rampe de #0f172a vers un ardoise voisin contient 52 couleurs et tient jusqu'à 156. Un hero sombre à la mode de #1a1a2e à #16213e ne contient que 28 couleurs et bande au-delà de 84 pixels — autrement dit, il bande dans toutes les mises en page où il apparaîtra. Une surface presque blanche de #ffffff à #f5f5f5 contient 11 couleurs et cède à 33 pixels ; tracée sur 1920, sa bande moyenne fait 174,5 pixels. Trois gestes pratiques : élargir la distance tonale entre les arrêts pour disposer de plus de niveaux, ajouter un léger bruit à un ou deux pour cent d'opacité pour tramer les frontières, ou orienter le dégradé en diagonale plutôt qu'en droite ligne, ce qui allonge la rampe par rapport à l'élément et désaligne les bords de bandes de la mise en page.
Que fait réellement `longer hue`, et quand le voudrait-on ?
Il force la teinte à faire le grand tour du cercle au lieu de prendre l'arc court. Dans une interpolation polaire, le navigateur a deux chemins candidats entre deux angles de teinte, et par défaut il choisit celui d'au plus 180°. En interpolant la teinte 20 vers la teinte 320, `shorter hue` donne un arc de −60° et passe par 5, 350 et 335 — un court passage dans les rouges. `longer hue` donne un arc de +300° et passe par 95, 170 et 245 — verts, cyans et bleus, autrement dit tout le spectre. Deux mêmes arrêts, dégradé entièrement différent. Le mot-clé mérite d'être sorti quand on veut un balayage arc-en-ciel à partir de deux extrémités sans écrire d'arrêts intermédiaires, ce qui est courant pour les indicateurs de progression, les rampes de visualisation de données et les bordures décoratives. Il existe deux autres mots-clés : `increasing hue` force l'angle à croître et `decreasing hue` à décroître, ce qui permet de contrôler le sens et non seulement la longueur. Attention au cas limite où les deux teintes sont exactement à 180° : shorter et longer décrivent alors la même longueur d'arc et il faut utiliser increasing ou decreasing pour dire de quel côté.
Pourquoi mon fondu vers la transparence paraît-il sale sur le bord ?
Presque toujours parce que l'arrêt transparent est un noir transparent. Le mot-clé CSS `transparent` se développe en rgb(0 0 0 / 0) : un dégradé écrit du blanc `to transparent` tire donc le canal de couleur du blanc vers le noir pendant que l'alpha diminue. Si couleur et alpha sont interpolés indépendamment, le milieu est un gris à 50 % avec un alpha de 0,5, qui se compose sur une page blanche en #bfbfbf — un voile gris visible. Interpoler en espace prémultiplié corrige cela : multiplie chaque couleur par son alpha avant le mélange, mélange, puis redivise ; un arrêt totalement transparent ne contribue plus à la couleur, ses canaux prémultipliés étant nuls. Le même milieu se compose alors en #ffffff sur blanc et #808080 sur noir. Les dégradés CSS sont spécifiés en prémultiplié : les navigateurs actuels sont donc corrects ici, mais le bogue survit dans le code canvas, en SVG, dans les éditeurs d'images et sur les moteurs anciens. Le correctif portable est d'écrire l'arrêt dans la couleur d'où tu pars — rgb(255 255 255 / 0) plutôt que `transparent` — ce qui donne des résultats identiques par l'une ou l'autre méthode et ne coûte rien.

Articles qui pourraient t'intéresser

Tous les guides
ExplicationLes harmonies de couleurs sont de la géométrie sur une roue — et la roue est fausseComplé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.ExplicationRapport de contraste : comment le WCAG le calcule vraimentLe rapport WCAG vaut (L1 + 0,05) ÷ (L2 + 0,05), et L est la luminance relative, pas la luminosité. Le vert en porte 71,52 % et le bleu 7,22 %, d'où le bleu pur sur blanc qui passe à 8,59:1 tandis que le gris moyen échoue à 3,95:1. Voici tout le calcul, exécuté de bout en bout.TutorielHEX vs RGB : lire et convertir les couleursHEX et RGB sont deux notations des mêmes couleurs. Voici comment lire chacune, convertir de l'une à l'autre et ajouter de la transparence.GuideCe qu'un minifieur peut retirer, et ce qu'il ne doit pas toucherLa minification doit préserver le sens, et l'intéressant, ce sont les espaces qui portent du sens : le combinateur de descendance, les espaces dans calc(), l'écart entre deux éléments en ligne. Mesuré ici sur de vrais fichiers, y compris ce que brotli te faisait déjà gagner.ExplicationComment marchent vraiment les polices Instagram (ce ne sont pas des polices)Rien n'est mis en forme. Chaque lettre est remplacée par un autre caractère Unicode qui ressemble à du gras ou du cursif — d'où un lecteur d'écran qui lit du charabia et des appareils qui affichent des carrés vides.ExplicationYAML a l'air aimable et il mordYAML, c'est JSON plus une couche d'inférence de types, et c'est l'inférence qui est dangereuse. Le même fichier passé dans un analyseur YAML 1.2 puis 1.1 : no est une chaîne dans l'un et false dans l'autre, 01234 vaut 1234 dans l'un et 668 dans l'autre, et 12:30:00 est un nombre dans l'un des deux.

Outils similaires

Sources

Tu as repéré une erreur dans cet article ?