Contraste de cores e acessibilidade: o que a WCAG exige
A WCAG pede contraste mínimo de 4,5:1 entre o texto e o fundo, e de 3:1 para texto grande e para ícones e bordas de campos, no nível AA, que é o exigido em leis e contratos. Este guia explica o que é a razão de contraste, como ela é calculada a partir da luminância das cores, os erros mais comuns (como arredondar 4,48 para 4,5) e como corrigir uma cor sem mudar a identidade visual.
Por que o contraste importa
Texto com pouco contraste é difícil de ler para quem tem baixa visão, daltonismo ou catarata, e também para qualquer pessoa no celular ao sol. A WCAG (Web Content Accessibility Guidelines), do W3C, define limites objetivos para isso, e é a referência usada pela Lei Brasileira de Inclusão, pelo eMAG do governo federal e por auditorias de acessibilidade.
O contraste também conta para a nota de acessibilidade do Lighthouse, que aponta cada texto abaixo do limite. Para conferir uma combinação na hora, use o verificador de contraste.
Os limites da WCAG 2.2
- Texto normal: 4,5:1 no nível AA e 7:1 no AAA.
- Texto grande (a partir de 24px, ou 18,66px em negrito): 3:1 no AA e 4,5:1 no AAA.
- Ícones, bordas de campos, foco e outros elementos gráficos necessários para entender a interface: 3:1.
- Ficam de fora logotipos, texto decorativo e elementos desabilitados.
O nível exigido na prática é o AA. O AAA é recomendado para textos longos e públicos específicos, mas nem sempre é possível em toda a interface.
Como a razão de contraste é calculada
Primeiro, cada canal da cor (vermelho, verde e azul, de 0 a 255) é convertido para luz linear, desfazendo a curva de gama do sRGB. Depois, a luminância relativa soma os canais com pesos diferentes, porque o olho é mais sensível ao verde: L = 0,2126 R + 0,7152 G + 0,0722 B. A razão é (L1 + 0,05) ÷ (L2 + 0,05), com L1 da cor mais clara. Preto sobre branco dá 21:1, o máximo.
// Razão de contraste da WCAG 2 entre duas cores em HEX ("#777777" e "#ffffff")
function luminancia(hex) {
const n = parseInt(hex.replace('#', ''), 16);
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {
const c = v / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; // canal em luz linear
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contraste(cor1, cor2) {
const [clara, escura] = [luminancia(cor1), luminancia(cor2)].sort((a, b) => b - a);
return (clara + 0.05) / (escura + 0.05);
}
const r = contraste('#777777', '#ffffff');
console.log(r.toFixed(2), r >= 4.5 ? 'passa no AA' : 'não passa no AA'); // 4.48 não passa no AA (a razão exata é 4,478...) Razão de contraste entre duas cores em HEX.
def luminancia(cor: str) -> float:
"""Luminância relativa da WCAG 2 de uma cor em HEX de 6 dígitos."""
h = cor.lstrip("#")
canais = []
for i in (0, 2, 4):
c = int(h[i : i + 2], 16) / 255
canais.append(c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4)
r, g, b = canais
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contraste(cor1: str, cor2: str) -> float:
clara, escura = sorted((luminancia(cor1), luminancia(cor2)), reverse=True)
return (clara + 0.05) / (escura + 0.05)
r = contraste("#777777", "#ffffff")
print(f"{r:.2f}", "passa no AA" if r >= 4.5 else "não passa no AA") # 4.48 não passa no AA (a razão exata é 4,478...) <?php
// Luminância relativa da WCAG 2 de uma cor em HEX de 6 dígitos
function luminancia(string $cor): float
{
[$r, $g, $b] = array_map(function (string $par): float {
$c = hexdec($par) / 255;
return $c <= 0.04045 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
}, str_split(ltrim($cor, '#'), 2));
return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}
function contraste(string $cor1, string $cor2): float
{
$l = [luminancia($cor1), luminancia($cor2)];
return (max($l) + 0.05) / (min($l) + 0.05);
}
$r = contraste('#777777', '#ffffff');
printf("%.2f %s\n", $r, $r >= 4.5 ? 'passa no AA' : 'não passa no AA'); // 4.48 não passa no AA (a razão exata é 4,478...) Erros comuns
- Arredondar para cima: #777777 sobre branco dá 4,478:1. Exibido com duas casas, vira 4,48, mas não chega a 4,5 e não passa no AA. Compare o valor exato com o limite.
- Esquecer a transparência: um texto preto com 50% de opacidade sobre branco equivale a um cinza médio. Calcule com a cor que aparece de fato, depois de sobrepor ao fundo.
- Testar só o tema claro: no modo escuro, as cores mudam e o contraste precisa ser conferido de novo.
- Confiar no placeholder: o texto de exemplo dentro de um campo também é texto, e costuma ficar claro demais.
Como corrigir uma cor sem mudar a identidade
Para manter a cor da marca e ainda passar, mude só a luz e preserve o tom. No espaço OKLCH, isso é direto: escurecer o azul #3b82f6 sobre branco até passar no AA dá um azul mais fechado, ainda reconhecível. O verificador de contraste faz essa busca e sugere a cor mais próxima que passa no AA e no AAA.
Outra saída é planejar a paleta com uma escala de tons, como a do gerador de paleta: os tons 50 a 100 servem de fundo, e os tons de 700 em diante, de texto. Para ver a cor em outros formatos, como HSL e OKLCH, use o conversor de cores.
Achou um erro neste guia? Avise por e-mail.
Ferramentas para usar junto
- Verificador de ContrasteConfira o contraste entre a cor do texto e a do fundo pela WCAG 2.2: razão, níveis AA e AAA e sugestões de cor que passam
- Gerador de Paleta a partir de HEXGere uma escala de tons de 50 a 950 e combinações (complementar, análogas, tríade) a partir de uma cor
- Conversor de Cores HEX RGB HSLConverta cores entre HEX, RGB, HSL, OKLCH, HWB, HSV e CMYK, com seletor de cores e os nomes do CSS
Perguntas frequentes
Qual o contraste mínimo para texto?
No nível AA da WCAG, 4,5:1 para texto normal e 3:1 para texto grande, a partir de 24px ou de 18,66px em negrito. No AAA, 7:1 e 4,5:1.
Cinza #777 sobre branco é acessível?
Para texto normal, não: a razão é 4,48:1, abaixo de 4,5. Passa para texto grande e para ícones. O cinza mais claro que passa no AA sobre branco é #767676.
O contraste do texto sobre imagem também conta?
Sim. Como a imagem varia, confira o contraste no pior ponto ou ponha uma camada escura ou clara por trás do texto, para garantir o mínimo em toda a área.
A WCAG 3 muda esses números?
A WCAG 3 está em elaboração e pode adotar outro método, o APCA. Enquanto ela não é norma, leis e auditorias seguem a WCAG 2, com os limites deste guia.