Do hexadecimal ao contraste: calcule se a cor do texto passa antes de publicar
Aprenda a calcular a razão de contraste a partir de dois códigos hex, saiba quais limites o WCAG pede para texto e botão, e ajuste a cor da marca até ela passar no claro e no escuro.
A loja de plantas escolheu o verde "seagreen", #2E8B57, direto de uma tabela de cores HTML. Combinava com o logo, ficou bonito no monitor do designer e foi para os parágrafos, os links e o botão "Comprar". Uma semana depois, uma cliente de 60 anos mandou mensagem dizendo que não conseguia ler as descrições no celular, na varanda. A cor não estava errada. O contraste estava: 4,25 para 1 sobre o branco, abaixo dos 4,5 que o WCAG pede para texto comum.
A resposta curta: o código hex diz qual é a cor, não se ela se lê. O que decide é a razão de contraste entre texto e fundo, um número de 1 a 21 calculado a partir dos dois hex. Texto normal precisa de pelo menos 4,5:1, texto grande e componentes de interface precisam de 3:1. Calcule para cada par que vai ao ar, no tema claro e no escuro, e escureça ou clareie a cor até passar.
Por que duas cores "fortes" podem não contrastar
O olho não percebe as três cores primárias da tela com a mesma intensidade. Somos muito mais sensíveis ao verde do que ao azul. Um azul puro parece escuro; um verde de mesma "força" parece claro. Por isso não dá para julgar contraste pelo nome da cor nem pela saturação. O que importa é a luminância relativa: quanto de luz aquela cor emite, do jeito que o olho percebe, numa escala de 0 (preto) a 1 (branco).
A tabela de cores ajudou a escolher o tom. Ela não sabe em cima de que fundo a cor vai ficar, nem em que tamanho. É a mesma ideia de o hexadecimal é o de menos, o contraste é o que se lê; aqui o foco é fazer a conta e decidir com o número na mão.
Como calcular a razão de contraste a partir do hex
A fórmula é a do WCAG 2 e cabe em cinco passos. O exemplo usa o verde da loja, #2E8B57, contra o branco, #FFFFFF.
- Separe o hex em vermelho, verde e azul e divida por 255.
2E= 46,8B= 139,57= 87. Divididos por 255: 0,1804; 0,5451; 0,3412. - Linearize cada canal. Se o valor for até 0,03928, divida por 12,92. Se for maior, calcule ((valor + 0,055) / 1,055) elevado a 2,4. Resultado: 0,0273; 0,2582; 0,0953.
- Some com os pesos da percepção. L = 0,2126 × R + 0,7152 × G + 0,0722 × B. Para o verde: 0,2126 × 0,0273 + 0,7152 × 0,2582 + 0,0722 × 0,0953 = 0,1973. O branco tem L = 1.
- Divida a mais clara pela mais escura, somando 0,05 em cada uma. (1 + 0,05) / (0,1973 + 0,05) = 1,05 / 0,2473 = 4,25.
- Compare com o limite. 4,25 é menos que 4,5. O verde reprova para texto de parágrafo e para o texto branco do botão. Passaria só em título grande.
Ninguém precisa fazer isso à mão toda vez. A função abaixo faz a mesma conta e serve para um teste automatizado ou para conferir uma paleta inteira:
function luminancia(hex) {
let h = hex.replace('#', '');
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
const [r, g, b] = [0, 2, 4]
.map((i) => parseInt(h.slice(i, i + 2), 16) / 255)
.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contraste(corA, corB) {
const [clara, escura] = [luminancia(corA), luminancia(corB)].sort((a, b) => b - a);
return (clara + 0.05) / (escura + 0.05);
}
console.log(contraste('#2E8B57', '#FFFFFF').toFixed(2)); // 4.25
console.log(contraste('#1F6B43', '#FFFFFF').toFixed(2)); // 6.48
console.log(contraste('#777', '#FFF').toFixed(2)); // 4.48
A última linha é um clássico: o cinza #777777 sobre branco dá 4,48, e reprova por muito pouco. #767676 dá 4,54 e passa. Contraste não perdoa arredondamento.
Quais limites valem para quê
- Texto normal: 4,5:1 no nível AA, que é o que a maioria das leis e contratos de acessibilidade usa como referência.
- Texto grande: 3:1. O WCAG considera grande a partir de 18 pontos, ou 14 pontos em negrito, o que dá cerca de 24 px e 18,7 px em CSS.
- Bordas de campo, ícones e estados de componente: 3:1 contra as cores vizinhas. Um campo de formulário com borda quase branca sobre fundo branco falha aqui.
- Nível AAA: 7:1 para texto normal e 4,5:1 para texto grande. Vale perseguir em textos longos e públicos mais velhos.
Existe um método novo de cálculo em discussão para versões futuras das diretrizes. Até ele virar referência, os números acima continuam sendo os cobrados em auditoria.
Como a loja ajustou a cor sem trocar de marca
- Separou cor de marca e cor de texto. O
#2E8B57continuou no logo e nas ilustrações, onde não precisa ser lido. - Escureceu o verde para texto até passar com folga.
#1F6B43dá 6,48 sobre branco e 5,94 sobre o cinza claro dos cards,#F5F5F5. O verde original daria só 3,89 sobre os cards. - Usou o verde escuro no fundo do botão. Texto branco sobre
#1F6B43dá os mesmos 6,48. - Refez a conta no tema escuro. O verde original sobre o fundo
#121212dá 4,41 e reprova. O tema escuro ganhou um verde mais claro,#3CB371, que dá 7,03 sobre esse fundo. - Guardou as cores por papel, não por nome.
:root {
--cor-marca: #2E8B57;
--cor-texto-destaque: #1F6B43;
--cor-fundo: #FFFFFF;
--cor-fundo-card: #F5F5F5;
}
@media (prefers-color-scheme: dark) {
:root {
--cor-texto-destaque: #3CB371;
--cor-fundo: #121212;
--cor-fundo-card: #1E1E1E;
}
}
a, .preco { color: var(--cor-texto-destaque); }
.botao-comprar { background: var(--cor-texto-destaque); color: var(--cor-fundo); }
Repare no botão: no tema escuro, o texto vira #121212 sobre #3CB371, e o par também precisa ser conferido. Toda dupla que aparece na tela entra na conta, inclusive as que nascem da troca de tema. Quando a paleta vem de um gerador automático, o processo é o mesmo, como em paleta gerada passa no contraste.
Para conferir no navegador, o seletor de cor do DevTools do Chrome mostra a razão de contraste do texto selecionado contra o fundo, e o inspetor de acessibilidade do Firefox aponta problemas de contraste na página.
Erros comuns
- Escolher a cor pelo nome na tabela. "Seagreen" e "forestgreen" parecem escuros e reprovam sobre branco para texto comum.
- Medir só o tema claro. A mesma cor de destaque sobre fundo escuro costuma cair abaixo do limite.
- Arredondar a seu favor. 4,48 não é 4,5. O cinza que "quase passa" reprova na auditoria e para quem lê.
- Medir o texto e esquecer o botão e a borda do campo. Componentes também têm limite, e um campo invisível não é preenchido.
- Usar a cor como única informação. Erro em vermelho sem texto nem ícone some para quem não distingue vermelho de verde.
- Guardar a cor com o nome dela na variável.
--verdevira mentira quando o tema escuro precisa de outro tom;--cor-texto-destaquecontinua verdadeira.
Checklist
- Todo par texto e fundo da página tem razão de contraste calculada.
- Texto de parágrafo e de botão passa de 4,5:1.
- Bordas de campo e ícones que funcionam como controle passam de 3:1.
- O tema escuro foi medido separadamente.
- As variáveis de cor têm nome de papel, não de cor.
- Há um teste ou script que recalcula o contraste quando a paleta muda.
Pegue agora a cor dos links do seu site e a cor de fundo onde eles aparecem e passe pela função acima: o número fica acima de 4,5 ou você estava confiando no olho?
Referência: http://www.cultura.ufpa.br/dicas/htm/htm-cor2.htm