Aller au contenu
Allin

Rapport de contraste : comment le WCAG le calcule vraiment

Publié le 15/05/2025 · 17 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

Le WCAG 2.x définit le contraste comme (L1 + 0,05) ÷ (L2 + 0,05), où L1 est la luminance relative de la couleur la plus claire et L2 celle de la plus sombre. La luminance relative n'est ni la luminosité ni le L de HSL. Chaque canal sRGB est divisé par 255, puis linéarisé par une fonction par morceaux — en dessous de 0,03928 on divise par 12,92, sinon on élève ((c + 0,055) ÷ 1,055) à la puissance 2,4 — et les trois valeurs linéaires sont pondérées 0,2126 rouge, 0,7152 vert, 0,0722 bleu. Le vert porte 71,52 % de la luminance et le bleu seulement 7,22 %, d'où le résultat que personne n'attend : le bleu pur #0000ff sur blanc obtient 8,59:1 et franchit tous les niveaux WCAG, tandis que le gris moyen #808080 sur blanc obtient 3,95:1 et échoue pour du texte courant. Le terme 0,05 explique que le noir sur blanc vaille 21:1 et non l'infini : le noir a une luminance de 0, et 1,05 ÷ 0,05 = 21. Les seuils sont 4,5:1 pour le texte courant en AA, 3:1 pour le grand texte (18 pt = 24 px CSS, ou 14 pt gras = 18,67 px) et pour les éléments non textuels, et 7:1 en AAA. Le WCAG 3 est un brouillon de travail précoce qui propose un autre modèle, perceptuel, appelé APCA ; il n'est pas normatif aujourd'hui.

Le 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.

La formule : deux luminances et une constante

Tout le contraste du WCAG 2.x tient en une ligne : rapport = (L1 + 0,05) ÷ (L2 + 0,05), où L1 est la luminance relative de la plus claire des deux couleurs et L2 celle de la plus sombre. Comme la spécification les ordonne par luminance et non par rôle, le rapport ne dépend jamais de celle que tu as appelée premier plan. Texte noir sur blanc et texte blanc sur noir donnent le même 21:1.

Tout l'intérêt est caché dans L. La luminance relative est une grandeur physique : elle estime la quantité de lumière émise par la surface, normalisée pour que le blanc pur vaille 1 et le noir pur 0. Ce n'est délibérément pas une mesure de la clarté perçue par l'œil humain, et ce n'est certainement pas le L de HSL ni le L de CIELAB, tous deux perceptuels et tous deux donnant des nombres très différents pour le même code hexadécimal.

Cette seule distinction explique presque toutes les disputes entre un designer et un vérificateur de contraste. L'outil ne juge pas la maquette : il divise deux nombres issus d'un modèle colorimétrique normalisé à l'époque des tubes cathodiques, qui n'a jamais prétendu coller à la perception.

Étape un : linéariser chaque canal

Une valeur sRGB de 128 ne porte pas la moitié de la lumière de 255. Les encodages d'affichage sont compandés en gamma pour dépenser la plage limitée de 8 bits là où la vision humaine est sensible : le nombre stocké est donc une fonction non linéaire de la lumière. Avant toute pondération ou addition, chaque canal doit être ramené à une grandeur linéaire.

Le WCAG écrit la fonction de transfert explicitement. Divise la valeur 8 bits par 255 pour obtenir c dans [0, 1]. Si c vaut au plus 0,03928, la valeur linéaire est c ÷ 12,92. Sinon, c'est ((c + 0,055) ÷ 1,055) élevé à la puissance 2,4. Le petit segment linéaire près du noir existe parce qu'une pure fonction puissance a une pente infinie en zéro, ce qui est numériquement gênant et physiquement faux pour un vrai écran : le sRGB y greffe donc une droite.

Exécute-la et la non-linéarité saute aux yeux : le canal 128 se linéarise en 0,215861, pas 0,5. Le canal 10 donne 0,003035, le canal 11 donne 0,003347. Une note de bas de page mérite d'être connue, car elle revient dans les rapports de bogue. Le WCAG écrit le seuil 0,03928 tandis que la norme sRGB de l'IEC écrit 0,04045. En 8 bits, ils coupent respectivement à 10,0164 et 10,31 : tous deux tombent entre les entiers 10 et 11 et aucun ne sélectionne jamais une branche différente. Un balayage des 16 777 216 couleurs sRGB face au blanc a produit zéro changement de verdict et une différence de rapport maximale exactement nulle. L'écart est réel dans le texte et sans effet sur aucun résultat 8 bits.

Étape deux : les poids, où le vert l'emporte

Les trois canaux linéaires sont combinés en L = 0,2126 R + 0,7152 V + 0,0722 B. Ces coefficients sont les poids de luminance de la Rec. 709 et leur somme fait exactement 1, ce qui place le blanc à 1,000000. Ils encodent un fait sur la rétine humaine, pas sur l'écran : les cônes qui dominent la perception de la luminance sont bien plus sensibles au milieu du spectre visible qu'à ses extrémités.

Les conséquences sont brutales dès qu'on calcule les primaires. Le rouge pur #ff0000 a une luminance relative de 0,212600. Le vert pur #00ff00 en a 0,715200. Le bleu pur #0000ff en a 0,072200. À intensité de canal identique, le vert vaut près de dix fois la luminance du bleu. Qu'une couleur passe un contrôle de contraste dépend donc bien plus de sa teneur en vert que de ce qu'un designer perçoit comme sa noirceur.

Un contrôle utile : cherche le gris de même luminance relative que le bleu pur. C'est #4c4c4c, à 0,072272 contre 0,072200 pour le bleu. Tous deux obtiennent 8,59:1 sur blanc. Pour le modèle, ces deux couleurs sont interchangeables du point de vue du contraste, ce qu'elles ne sont visiblement pas pour un lecteur — et cet écart est exactement la critique adressée au WCAG 2 par ceux qui construisent son successeur.

Le bleu pur passe, le gris moyen échoue

Passe les deux dans la formule. Bleu pur sur blanc : L1 = 1,000000, L2 = 0,072200, rapport = 1,05 ÷ 0,122200 = 8,59:1. Cela franchit l'AA du texte courant à 4,5:1 et l'AAA à 7:1. Gris moyen #808080 sur blanc : L2 = 0,215861, rapport = 1,05 ÷ 0,265861 = 3,95:1, ce qui échoue franchement à l'AA du texte courant et franchit tout juste la barre des 3:1 du grand texte. La plupart des gens à qui l'on demande de deviner classent ces deux couleurs dans l'ordre inverse.

La même pondération explique le vert pur. #00ff00 sur blanc obtient 1,37:1 — pire que presque toute couleur qu'un designer envisagerait — parce que le vert représente 71,52 % de la luminance et qu'un vert plein est donc presque aussi lumineux que le blanc. Sur noir, ce même vert obtient 15,30:1. Le bleu pur sur noir n'obtient que 2,44:1 et échoue même au seuil non textuel de 3:1, exactement l'inverse de son excellent résultat sur blanc.

Pour les gris, la frontière mérite d'être mémorisée. En balayant la rampe neutre face au blanc, #767676 donne 4,5422:1 et passe l'AA, tandis que la valeur immédiatement suivante #777777 donne 4,4781:1 et échoue. #767676 est donc le gris le plus clair capable de porter du texte courant sur fond blanc, et #595959 tombe sur 7,00:1, exactement la ligne AAA.

À quoi sert le 0,05

Le noir a une luminance relative de 0. Diviser quoi que ce soit par 0 donne un rapport infini, ce qui est à la fois inutile comme score et faux comme description d'un écran réel. Le 0,05 ajouté aux deux termes représente la lumière ambiante réfléchie par la surface de l'écran : un pixel noir n'est jamais vraiment noir dans une pièce éclairée, et la constante approxime le voile qui le relève.

L'arithmétique est immédiate. Le blanc sur noir devient (1 + 0,05) ÷ (0 + 0,05) = 1,05 ÷ 0,05 = 21. Voilà d'où vient le fameux plafond de 21:1 — ce n'est pas une décision de conception, c'est 1,05 divisé par 0,05. Retire la constante et la même paire s'évalue à l'infini, ce que renvoie une implémentation naïve et ce que plusieurs premières bibliothèques de contraste ont réellement livré.

La constante comprime aussi l'extrémité sombre de l'échelle. Deux couleurs presque noires dont la luminance diffère d'un facteur dix ont des rapports de contraste presque identiques, car 0,05 domine les deux dénominateurs. C'est volontaire — à ces niveaux, la différence est réellement invisible sous une lumière normale — mais cela explique pourquoi les palettes de mode sombre semblent plus difficiles à régler : la formule cesse de discriminer précisément là où vivent les thèmes sombres.

Les seuils, et ce que grand texte signifie en chiffres

Le critère 1.4.3 (Contraste minimum, niveau AA) exige 4,5:1 pour le texte et les images de texte, avec une descente à 3:1 pour le grand texte. Le critère 1.4.6 (Contraste amélioré, niveau AAA) porte ces valeurs à 7:1 et 4,5:1. Le critère 1.4.11 (Contraste non textuel, niveau AA) exige 3:1 pour les composants d'interface et les objets graphiques nécessaires à la compréhension — bordures de formulaire, indicateurs de focus, glyphes d'icônes, segments de graphiques.

Le grand texte est défini en points, pas en pixels : au moins 18 points, ou au moins 14 points en gras. En convertissant à la référence CSS de 1 pt = 4/3 px, cela donne 24 px et 18,67 px en gras. Ce sont les nombres à vérifier dans une feuille de style. Un titre de 20 px n'est pas du grand texte selon cette définition et exige toujours 4,5:1, ce qui piège quantité de maquettes ayant supposé les titres exemptés.

Deux exemptions méritent d'être connues, car elles sont fréquemment mal lues. Le texte appartenant à un composant d'interface inactif, la pure décoration ou ce qui est invisible pour tout le monde n'ont aucune exigence de contraste. Et les logotypes — texte faisant partie d'un logo ou d'un nom de marque — sont eux aussi exemptés. Aucune de ces exemptions ne couvre un texte indicatif peu contrasté dans un champ de formulaire ni une étiquette grisée que les utilisateurs sont malgré tout censés lire.

Où se situent l'APCA et le WCAG 3

Le résultat gris-bleu ci-dessus n'est pas un bogue d'implémentation : c'est une limite du modèle. Un rapport de luminance ignore la teinte, la saturation, la graisse de la police, l'épaisseur des traits et laquelle des deux couleurs est au-dessus — autant de facteurs qui affectent mesurablement la lisibilité. Un caractère fin en graisse 400 à 4,5:1 peut être plus difficile à lire qu'un caractère épais à 4:1, et la formule ne peut pas le dire.

L'APCA, Accessible Perceptual Contrast Algorithm, est la réponse. Elle modélise la clarté perçue plutôt que la luminance physique, tient délibérément compte de la polarité — sombre sur clair et clair sur sombre donnent des scores différents pour la même paire — et produit une valeur signée sur une échelle allant approximativement de moins 108 à plus 106 plutôt qu'un rapport, la cible dépendant conjointement de la taille et de la graisse de la police.

Son statut demande de la précision. Le WCAG 3 est un brouillon de travail du W3C — un document précoce, explicitement instable, dont le W3C lui-même avertit qu'il ne doit pas servir de base à une implémentation. L'APCA y figure comme méthode candidate, pas comme exigence ratifiée. Nulle part aujourd'hui une évaluation légale ou contractuelle ne se fait contre l'APCA. Le WCAG 2.2 est la recommandation W3C en vigueur, et c'est elle que référencent réellement les politiques d'accessibilité, les marchés publics et les contentieux. Sers-toi de l'APCA comme d'un second avis quand le rapport et tes yeux divergent ; livre contre les nombres du 2.x.

L la plus sombre
Paires réelles calculées avec la définition WCAG 2.x : luminances relatives, rapport obtenu et verdict à chaque seuil
PaireL la plus claireL la plus sombreRapportAA texte 4,5:1AA grand 3:1AAA texte 7:1
#000000 sur #ffffff1,0000000,00000021,00:1RéussiRéussiRéussi
#0000ff sur #ffffff1,0000000,0722008,59:1RéussiRéussiRéussi
#595959 sur #ffffff1,0000000,0998997,00:1RéussiRéussiRéussi (pile sur la ligne)
#767676 sur #ffffff1,0000000,1811644,54:1Réussi (gris le plus clair qui y parvient)RéussiÉchec
#777777 sur #ffffff1,0000000,1844754,48:1Échec (un cran plus sombre passe)RéussiÉchec
#ff0000 sur #ffffff1,0000000,2126004,00:1ÉchecRéussiÉchec
#808080 sur #ffffff1,0000000,2158613,95:1ÉchecRéussiÉchec
#00ff00 sur #ffffff1,0000000,7152001,37:1ÉchecÉchecÉchec
Vérificateur de contraste des couleursRapport de contraste WCAG entre texte et fond, avec réussite/échec AA/AAA et un aperçu en direct.Essayer l'outil

Questions fréquentes

Pourquoi le bleu pur sur blanc passe-t-il alors que le gris moyen échoue ?
Parce que la formule pondère le vert à 0,7152 et le bleu à seulement 0,0722. Le bleu pur n'a ni rouge ni vert : sa luminance relative vaut donc 0,072200 — très sombre selon le modèle, même s'il paraît vif. Le gris moyen #808080 a des quantités égales des trois canaux et, après linéarisation, sa luminance vaut 0,215861, trois fois plus. Face au blanc, cela donne 8,59:1 pour le bleu et 3,95:1 pour le gris : le bleu franchit AA et AAA tandis que le gris échoue au texte courant AA. Rien n'a dysfonctionné : la luminance relative mesure une émission de lumière, pas l'aspect coloré ou sombre. La leçon pratique est d'arrêter de deviner. Les bleus, violets et rouges profonds saturés obtiennent régulièrement de meilleurs scores que leur apparence ne le laisse croire, et les jaunes, cyans et verts clairs de bien pires. Avec une palette de marque fortement colorée, mesure chaque paire plutôt que de juger à l'œil : l'intuition sur ce qui est sombre est entraînée sur la clarté perçue, et la formule ne l'est pas.
Le rapport change-t-il si j'inverse la couleur de premier plan et celle de fond ?
Non. Le WCAG 2.x définit L1 comme la luminance relative de la couleur la plus claire et L2 comme celle de la plus sombre : l'ordre est donc décidé par les luminances elles-mêmes, pas par celle qui sert au texte. Texte noir sur blanc et texte blanc sur noir donnent tous deux 21:1. Texte bleu #0000ff sur blanc et texte blanc sur bleu donnent tous deux 8,59:1. Cette symétrie est une vraie propriété du modèle 2.x et une vraie limite, car la polarité affecte bel et bien la lisibilité : un texte clair sur fond sombre paraît plus maigre et peut baver sur certains écrans, un effet que le rapport ne sait pas représenter. L'APCA, algorithme candidat du brouillon WCAG 3, brise délibérément cette symétrie et rapporte des valeurs différentes selon l'ordre. Sous WCAG 2.x, en revanche, un vérificateur qui donne une autre réponse quand on inverse les champs a un bogue.
Quel est le gris le plus clair utilisable pour du texte courant sur blanc ?
#767676. En balayant la rampe neutre face à #ffffff, cette valeur produit 4,5422:1 et franchit le seuil AA de 4,5:1. Le cran suivant, #777777, produit 4,4781:1 et échoue. Il n'existe aucun gris entre les deux — ce sont des valeurs 8 bits adjacentes — donc #767676 est exactement la frontière. Deux conséquences pratiques. D'abord, si ton système de design utilise un jeton comme grey-500 pour le texte secondaire, vérifie son hexadécimal réel : quantité de systèmes atterrissent entre 0x77 et 0x99 et sont non conformes pour du texte courant tout en paraissant parfaitement délibérés. Ensuite, pour l'AAA, la frontière équivalente est #595959, qui donne exactement 7,00:1. Les deux chiffres supposent un fond blanc pur ; sur une surface blanc cassé comme #fafafa, toute l'échelle se décale et il faut recalculer. C'est à cela que sert le vérificateur de contraste : la frontière bouge avec le fond, et les valeurs mémorisées cessent d'être vraies dès que le fond change.
Les icônes, bordures de formulaire et anneaux de focus exigent-ils aussi 4,5:1 ?
Non — ils relèvent du critère 1.4.11, Contraste non textuel, qui demande 3:1 au niveau AA. Il s'applique à deux choses : l'information visuelle nécessaire pour identifier un composant d'interface et son état, et les objets graphiques nécessaires à la compréhension du contenu. La bordure qui indique où commence un champ de saisie exige donc 3:1 face à son entourage, tout comme un indicateur de focus, une case cochée, un interrupteur en position active et les segments de couleurs différentes d'un graphique que le lecteur doit distinguer. Ce qui n'est pas couvert : la décoration, l'état inactif d'un composant, ou les graphiques dont une présentation précise est essentielle — photographie, drapeau, capture d'écran d'un autre produit. Note que la comparaison se fait avec les couleurs adjacentes : pour un anneau de focus, le fond sur lequel il repose ; pour une icône sur un bouton coloré, le remplissage du bouton, pas la page. Ce seul critère révèle plus de défauts réels dans les bibliothèques de composants modernes que les règles de texte, car les champs dessinés avec une bordure grise claire de 1 px sont quasi universels.
L'APCA remplace-t-elle la règle des 4,5:1, et faut-il basculer maintenant ?
Pas encore, et peut-être pas sous cette forme. Le WCAG 3 est un brouillon de travail du W3C, statut qui signifie explicitement instable et impropre à servir de base d'implémentation ; son propre texte prévient de ne pas le citer comme acquis. L'APCA y figure comme méthode candidate en évaluation, avec des questions ouvertes sur les seuils et sur la manière même dont la conformité serait notée. Pendant ce temps, le WCAG 2.2 est une recommandation W3C, et c'est le WCAG 2.x que visent les législations d'accessibilité, les règles de marchés publics et les plaintes juridiques dans le monde. Rien n'est évalué contre l'APCA aujourd'hui. La position raisonnable est de continuer à se conformer au 2.x — 4,5:1, 3:1, 7:1, calculés comme ci-dessus — et de traiter l'APCA comme un diagnostic quand le rapport et tes yeux divergent, ce qui arrive surtout avec les teintes saturées et les caractères fins sur fond sombre. Si l'APCA juge une paire pire que le rapport ne le suggère, cela mérite d'agir ; si elle la juge meilleure, tu ne peux toujours pas livrer sous 4,5:1 et revendiquer l'AA.
Comment calculer le rapport à la main pour une couleur précise ?
Prenons #1a73e8 sur blanc comme exemple travaillé. Découpe l'hexadécimal en canaux : 26, 115, 232. Divise chacun par 255 : 0,101961, 0,450980, 0,909804. Les trois dépassent 0,03928, donc les trois passent par la branche puissance : ((c + 0,055) ÷ 1,055) à la 2,4, ce qui donne 0,010330, 0,171441, 0,806952. Applique les poids : 0,2126 × 0,010330 = 0,002196, 0,7152 × 0,171441 = 0,122615, 0,0722 × 0,806952 = 0,058262. Additionne pour L = 0,183073. Le blanc vaut 1,000000 et c'est la plus claire, donc le rapport est (1 + 0,05) ÷ (0,183073 + 0,05) = 1,05 ÷ 0,233073 = 4,51:1. Cela passe l'AA du texte courant de six centièmes, ce qui mérite attention : une couleur aussi proche de la ligne échouera dès que quelqu'un l'éclaircira légèrement, donc traite la plage 4,5 à 4,6 comme une zone d'alerte plutôt qu'une réussite. Toute la procédure tient en douze lignes de code dans n'importe quel langage, d'où l'accord de tous les vérificateurs à la deuxième décimale — la seule vraie source de divergence entre outils est l'arrondi.

Articles qui pourraient t'intéresser

Tous les guides
ExplicationDégradés, banding, et pourquoi le milieu paraît terneInterpoler 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.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.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.ExplicationCe qu'un pourcentage de disponibilité autorise réellementTrois neuf, cela sonne comme une promesse — jusqu'à ce qu'on le divise en minutes. Ce que 99,9 % achète par an, par mois, par semaine et par jour ; pourquoi la fenêtre de mesure compte bien plus que le neuf supplémentaire ; et les deux mois différents que cet outil utilise pour un seul et même identifiant.

Outils similaires

Sources

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