Rácio de contraste: como a WCAG o calcula realmente
Publicado a 15/05/2025 · 17 min de leitura · Ferramentas para programadores
Daniel Okonkwo — Programador front-end e redator de Tecnologia na Allin
Desempenho web · Formatos de ficheiro
Verificado a partir de 6 fontes
A WCAG 2.x define o contraste como (L1 + 0,05) ÷ (L2 + 0,05), em que L1 é a luminância relativa da cor mais clara e L2 a da mais escura. A luminância relativa não é o brilho nem o L do HSL. Cada canal sRGB é dividido por 255 e depois linearizado com uma função por troços — abaixo de 0,03928 divide-se por 12,92, caso contrário eleva-se ((c + 0,055) ÷ 1,055) a 2,4 — e os três valores lineares são ponderados 0,2126 vermelho, 0,7152 verde, 0,0722 azul. O verde carrega 71,52 % da luminância e o azul apenas 7,22 %, o que produz o resultado que ninguém espera: o azul puro #0000ff sobre branco obtém 8,59:1 e ultrapassa todos os níveis WCAG, enquanto o cinzento médio #808080 sobre branco obtém 3,95:1 e falha para texto corrente. O termo 0,05 é a razão de o preto sobre branco ser 21:1 e não infinito: o preto tem luminância 0, e 1,05 ÷ 0,05 = 21. Os limiares são 4,5:1 para texto de corpo em AA, 3:1 para texto grande (18 pt = 24 px CSS, ou 14 pt a negrito = 18,67 px) e para elementos não textuais, e 7:1 em AAA. A WCAG 3 é um rascunho de trabalho inicial que propõe outro modelo, percetual, chamado APCA; hoje não é normativo.
O rácio WCAG é (L1 + 0,05) ÷ (L2 + 0,05), e L é a luminância relativa, não o brilho. O verde carrega 71,52 % e o azul 7,22 %, razão pela qual o azul puro sobre branco passa com 8,59:1 enquanto o cinzento médio falha com 3,95:1. Eis o cálculo inteiro, corrido de ponta a ponta.
A fórmula: duas luminâncias e uma constante
Todo o contraste da WCAG 2.x cabe numa linha: rácio = (L1 + 0,05) ÷ (L2 + 0,05), em que L1 é a luminância relativa da mais clara das duas cores e L2 a da mais escura. Como a especificação as ordena por luminância e não por papel, o rácio nunca depende de qual chamou primeiro plano. Texto preto sobre branco e texto branco sobre preto dão o mesmo 21:1.
Tudo o que interessa está escondido dentro de L. A luminância relativa é uma grandeza física: estima quanta luz a superfície emite, normalizada para que o branco puro valha 1 e o preto puro 0. Deliberadamente não é uma medida de quão claro a cor parece ao olho humano, e certamente não é o L do HSL nem o L do CIELAB, ambos percetuais e ambos com números muito diferentes para o mesmo código hexadecimal.
Essa única distinção explica quase todas as discussões de um designer com um verificador de contraste. A ferramenta não julga o design: divide dois números de um modelo colorimétrico normalizado na era dos tubos de raios catódicos, que nunca pretendeu coincidir com a perceção.
Passo um: linearizar cada canal
Um valor sRGB de 128 não transporta metade da luz de 255. As codificações de ecrã são comprimidas em gama para gastar o alcance limitado de 8 bits onde a visão humana é sensível: o número guardado é, portanto, uma função não linear da luz. Antes de qualquer ponderação ou soma, cada canal tem de ser revertido para uma grandeza linear.
A WCAG escreve a função de transferência de forma explícita. Divida o valor de 8 bits por 255 para obter c em [0, 1]. Se c for no máximo 0,03928, o valor linear é c ÷ 12,92. Caso contrário, é ((c + 0,055) ÷ 1,055) elevado a 2,4. O pequeno troço linear junto ao preto existe porque uma função potência pura tem declive infinito em zero, o que é numericamente incómodo e fisicamente falso para um ecrã real: o sRGB enxerta ali uma reta.
Execute-a e a não linearidade salta à vista: o canal 128 lineariza para 0,215861, não 0,5. O canal 10 dá 0,003035 e o canal 11 dá 0,003347. Há aqui uma nota que vale a pena conhecer, porque aparece nos relatórios de erro. A WCAG escreve o limiar 0,03928 enquanto a norma sRGB da IEC escreve 0,04045. Em 8 bits cortam em 10,0164 e 10,31 respetivamente, pelo que ambos caem entre os inteiros 10 e 11 e nenhum seleciona alguma vez um ramo diferente. Uma varredura das 16 777 216 cores sRGB face ao branco produziu zero mudanças de veredicto e uma diferença máxima de rácio exatamente nula. A discrepância é real no texto e não afeta nenhum resultado de 8 bits.
Passo dois: os pesos, onde o verde ganha
Os três canais lineares combinam-se como L = 0,2126 R + 0,7152 V + 0,0722 A. Esses coeficientes são os pesos de luminância da Rec. 709 e somam exatamente 1, o que coloca o branco em 1,000000. Codificam um facto sobre a retina humana, não sobre o ecrã: os cones que dominam a perceção da luminância são muito mais sensíveis ao centro do espetro visível do que às suas pontas.
As consequências são cruas assim que se calculam as primárias. O vermelho puro #ff0000 tem luminância relativa 0,212600. O verde puro #00ff00 tem 0,715200. O azul puro #0000ff tem 0,072200. À mesma intensidade de canal, o verde vale quase dez vezes a luminância do azul. Que uma cor passe uma verificação de contraste depende, portanto, muito mais do seu teor de verde do que daquilo que um designer perceciona como a sua escuridão.
Uma verificação útil: procure o cinzento com a mesma luminância relativa do azul puro. É #4c4c4c, com 0,072272 contra 0,072200 do azul. Ambos obtêm 8,59:1 sobre branco. Para o modelo, essas duas cores são intercambiáveis para efeitos de contraste, coisa que visivelmente não são para um leitor — e essa lacuna é exatamente a crítica feita à WCAG 2 por quem constrói a sua sucessora.
O azul puro passa, o cinzento médio falha
Passe as duas pela fórmula. Azul puro sobre branco: L1 = 1,000000, L2 = 0,072200, rácio = 1,05 ÷ 0,122200 = 8,59:1. Isso ultrapassa o AA de texto de corpo com 4,5:1 e o AAA com 7:1. Cinzento médio #808080 sobre branco: L2 = 0,215861, rácio = 1,05 ÷ 0,265861 = 3,95:1, que falha em cheio o AA de texto de corpo e mal ultrapassa a fasquia de 3:1 do texto grande. A maioria das pessoas, se lhes pedir para adivinhar, ordena essas duas cores ao contrário.
A mesma ponderação explica o verde puro. #00ff00 sobre branco obtém 1,37:1 — pior do que quase qualquer cor que um designer consideraria — porque o verde é 71,52 % da luminância e um verde pleno é, portanto, quase tão luminoso como o branco. Sobre preto, esse mesmo verde obtém 15,30:1. O azul puro sobre preto obtém apenas 2,44:1 e falha até o limiar não textual de 3:1, exatamente o inverso do seu excelente resultado sobre branco.
Para os cinzentos, a fronteira merece ser memorizada. Varrendo a rampa neutra face ao branco, #767676 dá 4,5422:1 e passa o AA, enquanto o valor imediatamente seguinte #777777 dá 4,4781:1 e falha. O #767676 é, portanto, o cinzento mais claro capaz de levar texto de corpo sobre fundo branco, e o #595959 cai em 7,00:1, exatamente a linha AAA.
Para que serve o 0,05
O preto tem luminância relativa 0. Dividir o que quer que seja por 0 dá um rácio infinito, coisa inútil como pontuação e falsa como descrição de um ecrã real. O 0,05 acrescentado a ambos os termos representa a luz ambiente refletida na superfície do ecrã: um píxel preto nunca é verdadeiramente preto numa sala iluminada, e a constante aproxima o véu que o levanta.
A aritmética é imediata. O branco sobre preto passa a ser (1 + 0,05) ÷ (0 + 0,05) = 1,05 ÷ 0,05 = 21. É daí que vem o famoso teto de 21:1 — não é uma decisão de desenho, é 1,05 dividido por 0,05. Retire a constante e o mesmo par avalia para infinito, que é o que uma implementação ingénua devolve e o que várias bibliotecas de contraste iniciais chegaram a publicar.
A constante também comprime o extremo escuro da escala. Duas cores quase pretas cuja luminância difere por um fator dez têm rácios de contraste quase idênticos, porque 0,05 domina ambos os denominadores. É intencional — a esses níveis a diferença é realmente invisível com luz normal — mas explica por que as paletas de modo escuro parecem mais difíceis de afinar: a fórmula deixa de discriminar precisamente onde vivem os temas escuros.
Os limiares e o que texto grande significa em números
O critério 1.4.3 (Contraste mínimo, nível AA) exige 4,5:1 para texto e imagens de texto, descendo para 3:1 no texto grande. O critério 1.4.6 (Contraste melhorado, nível AAA) sobe esses valores para 7:1 e 4,5:1. O critério 1.4.11 (Contraste não textual, nível AA) exige 3:1 para componentes de interface e objetos gráficos necessários à compreensão — contornos de formulário, indicadores de foco, glifos de ícones, segmentos de gráficos.
O texto grande é definido em pontos, não em píxeis: pelo menos 18 pontos, ou pelo menos 14 pontos se a negrito. Convertendo com a referência CSS de 1 pt = 4/3 px dá 24 px e 18,67 px a negrito. São esses os números a verificar numa folha de estilos. Um título de 20 px não é texto grande segundo esta definição e continua a precisar de 4,5:1, o que apanha imensos designs que assumiram os títulos isentos.
Duas isenções merecem ser conhecidas, porque são frequentemente mal lidas. O texto que faz parte de um componente de interface inativo, a pura decoração ou o que é invisível para todos não têm requisito de contraste. E os logótipos — texto que faz parte de um logótipo ou nome de marca — também estão isentos. Nenhuma dessas isenções cobre um texto de sugestão pouco contrastado num campo de formulário nem uma etiqueta esbatida que os utilizadores ainda têm de ler.
Onde encaixam a APCA e a WCAG 3
O resultado cinzento-azul acima não é um erro de implementação: é uma limitação do modelo. Um rácio de luminância ignora o matiz, a saturação, a espessura da fonte, a largura do traço e qual das duas cores está por cima — todos fatores que afetam de forma mensurável a legibilidade. Um tipo fino de peso 400 a 4,5:1 pode ser mais difícil de ler do que um tipo grosso a 4:1, e a fórmula não o consegue dizer.
A APCA, Accessible Perceptual Contrast Algorithm, é a resposta. Modela a claridade percecionada em vez da luminância física, tem deliberadamente em conta a polaridade — escuro sobre claro e claro sobre escuro dão pontuações diferentes para o mesmo par — e produz um valor com sinal numa escala aproximada de menos 108 a mais 106 em vez de um rácio, com o alvo a depender conjuntamente do tamanho e do peso da fonte.
O seu estatuto exige precisão. A WCAG 3 é um rascunho de trabalho do W3C — um documento inicial, explicitamente instável, sobre o qual o próprio W3C avisa que não deve servir de base a implementação. A APCA figura aí como método candidato, não como requisito ratificado. Hoje não há em lado nenhum uma avaliação legal ou contratual contra a APCA. A WCAG 2.2 é a recomendação W3C em vigor e é a que as políticas de acessibilidade, a contratação pública e os litígios realmente referenciam. Use a APCA como segunda opinião quando o rácio e os seus olhos discordarem; publique contra os números da 2.x.
| Par | L mais clara | L mais escura | Rácio | AA corpo 4,5:1 | AA grande 3:1 | AAA corpo 7:1 |
|---|---|---|---|---|---|---|
| #000000 sobre #ffffff | 1,000000 | 0,000000 | 21,00:1 | Passa | Passa | Passa |
| #0000ff sobre #ffffff | 1,000000 | 0,072200 | 8,59:1 | Passa | Passa | Passa |
| #595959 sobre #ffffff | 1,000000 | 0,099899 | 7,00:1 | Passa | Passa | Passa (exatamente na linha) |
| #767676 sobre #ffffff | 1,000000 | 0,181164 | 4,54:1 | Passa (o cinzento mais claro que consegue) | Passa | Falha |
| #777777 sobre #ffffff | 1,000000 | 0,184475 | 4,48:1 | Falha (um passo mais escuro passa) | Passa | Falha |
| #ff0000 sobre #ffffff | 1,000000 | 0,212600 | 4,00:1 | Falha | Passa | Falha |
| #808080 sobre #ffffff | 1,000000 | 0,215861 | 3,95:1 | Falha | Passa | Falha |
| #00ff00 sobre #ffffff | 1,000000 | 0,715200 | 1,37:1 | Falha | Falha | Falha |
Perguntas frequentes
- Porque é que o azul puro sobre branco passa e o cinzento médio falha?
- Porque a fórmula pondera o verde com 0,7152 e o azul com apenas 0,0722. O azul puro não tem nada de vermelho nem de verde, pelo que a sua luminância relativa é 0,072200 — muito escuro segundo o modelo, ainda que pareça vivo. O cinzento médio #808080 tem quantidades iguais dos três canais e, depois de linearizar, a sua luminância é 0,215861, três vezes mais. Face ao branco isso dá 8,59:1 para o azul e 3,95:1 para o cinzento: o azul ultrapassa AA e AAA enquanto o cinzento falha o texto de corpo AA. Nada correu mal: a luminância relativa mede emissão de luz, não o quão colorido ou escuro algo parece. A lição prática é deixar de adivinhar. Os azuis, roxos e vermelhos profundos saturados pontuam sistematicamente melhor do que o seu aspeto sugere, e os amarelos, cianos e verdes claros muito pior. Com uma paleta de marca muito colorida, meça cada par em vez de julgar a olho: a intuição sobre o que é escuro está treinada na claridade percecionada, e a fórmula não está.
- O rácio muda se eu trocar a cor de primeiro plano com a de fundo?
- Não. A WCAG 2.x define L1 como a luminância relativa da cor mais clara e L2 como a da mais escura, pelo que a ordem é decidida pelas próprias luminâncias, não por qual delas pintou o texto. Texto preto sobre branco e texto branco sobre preto dão ambos 21:1. Texto azul #0000ff sobre branco e texto branco sobre azul dão ambos 8,59:1. Essa simetria é uma propriedade real do modelo 2.x e uma limitação real, porque a polaridade afeta mesmo a legibilidade: texto claro sobre fundo escuro tende a parecer mais fino e pode esborratar em alguns ecrãs, um efeito que o rácio não consegue representar. A APCA, o algoritmo candidato do rascunho da WCAG 3, quebra essa simetria de propósito e devolve valores diferentes conforme a ordem. Sob a WCAG 2.x, porém, um verificador que dê outra resposta ao trocar os campos tem um erro.
- Qual é o cinzento mais claro que posso usar para texto de corpo sobre branco?
- #767676. Varrendo a rampa neutra face a #ffffff, esse valor produz 4,5422:1 e ultrapassa o limiar AA de 4,5:1. O passo seguinte, #777777, produz 4,4781:1 e falha. Não há nenhum cinzento entre os dois — são valores de 8 bits adjacentes — pelo que #767676 é exatamente a fronteira. Duas consequências práticas. Primeiro, se o seu sistema de design usa um token como grey-500 para texto secundário, verifique o hexadecimal real: imensos sistemas caem entre 0x77 e 0x99 e não cumprem para texto de corpo, parecendo perfeitamente deliberados. Segundo, se precisar de AAA, a fronteira equivalente é #595959, que dá exatamente 7,00:1. Ambos os números pressupõem fundo branco puro; sobre uma superfície branco sujo como #fafafa toda a escala se desloca e é preciso recalcular. É para isso que serve o verificador de contraste: a fronteira move-se com o fundo, e os valores memorizados deixam de ser verdadeiros assim que o fundo muda.
- Os ícones, contornos de formulário e anéis de foco também precisam de 4,5:1?
- Não — caem sob o critério 1.4.11, Contraste não textual, que pede 3:1 no nível AA. Aplica-se a duas coisas: a informação visual necessária para identificar um componente de interface e o seu estado, e os objetos gráficos necessários para compreender o conteúdo. Assim, o contorno que indica onde começa um campo de texto precisa de 3:1 face ao que o rodeia, tal como um indicador de foco, uma caixa marcada, um interruptor na posição ativa e os segmentos de cor diferente de um gráfico que o leitor tem de distinguir. O que não cobre: a decoração, o estado inativo de um componente, ou os gráficos cuja apresentação concreta é essencial — uma fotografia, uma bandeira, uma captura de outro produto. Atenção: a comparação é com as cores adjacentes, o que para um anel de foco significa o fundo em que assenta e para um ícone num botão colorido significa o preenchimento do botão, não a página. Este único critério apanha mais defeitos reais nas bibliotecas de componentes modernas do que as regras de texto, porque os campos desenhados com um contorno cinzento claro de 1 px são quase universais.
- A APCA vem substituir a regra dos 4,5:1? Devo mudar já?
- Ainda não, e talvez não nessa forma. A WCAG 3 é um rascunho de trabalho do W3C, estatuto que significa explicitamente instável e impróprio como base de implementação; o próprio texto avisa que não deve ser citado como algo assente. A APCA aparece aí como método candidato em avaliação, a par de perguntas em aberto sobre os limiares e sobre como sequer se pontuaria a conformidade. Entretanto a WCAG 2.2 é uma recomendação do W3C, e é a WCAG 2.x que as legislações de acessibilidade, as regras de contratação pública e as queixas judiciais no mundo inteiro apontam. Hoje nada é avaliado contra a APCA. A posição sensata é continuar a cumprir a 2.x — 4,5:1, 3:1, 7:1, calculados como acima — e tratar a APCA como diagnóstico quando o rácio e os seus olhos discordarem, o que acontece sobretudo com matizes saturados e com tipos finos sobre fundos escuros. Se a APCA disser que um par é pior do que o rácio sugere, vale a pena agir; se disser que é melhor, continua a não poder publicar abaixo de 4,5:1 e reclamar AA.
- Como calculo o rácio à mão para uma cor concreta?
- Tome #1a73e8 sobre branco como exemplo resolvido. Divida o hexadecimal em canais: 26, 115, 232. Divida cada um por 255: 0,101961, 0,450980, 0,909804. Os três excedem 0,03928, pelo que os três seguem o ramo de potência: ((c + 0,055) ÷ 1,055) elevado a 2,4, dando 0,010330, 0,171441, 0,806952. Aplique os pesos: 0,2126 × 0,010330 = 0,002196, 0,7152 × 0,171441 = 0,122615, 0,0722 × 0,806952 = 0,058262. Some-os para L = 0,183073. O branco é 1,000000 e é o mais claro, logo o rácio é (1 + 0,05) ÷ (0,183073 + 0,05) = 1,05 ÷ 0,233073 = 4,51:1. Isso passa o AA de texto de corpo por seis centésimos, o que merece atenção: uma cor tão perto da linha falhará assim que alguém a clarear um pouco, por isso trate a faixa 4,5 a 4,6 como zona de aviso e não como aprovação. Todo o procedimento são doze linhas de código em qualquer linguagem, e é por isso que todos os verificadores concordam até à segunda casa decimal — a única fonte real de divergência entre ferramentas é o arredondamento.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Fontes
- W3C — Web Content Accessibility Guidelines (WCAG) 2.2 — relative luminance and contrast ratio definitions
- W3C WAI — Understanding Success Criterion 1.4.3: Contrast (Minimum)
- W3C WAI — Understanding Success Criterion 1.4.6: Contrast (Enhanced)
- W3C WAI — Understanding Success Criterion 1.4.11: Non-text Contrast
- W3C — W3C Accessibility Guidelines (WCAG) 3.0 — Working Draft (APCA appears here as a candidate method)
- IEC — IEC 61966-2-1:1999 — Default RGB colour space (sRGB), source of the piecewise transfer function
Detetaste um erro neste artigo?