Programação Por Equipe DevBras

Capa do artigo: Do hexadecimal ao contraste: calcule se a cor do texto passa antes de publicar

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Separou cor de marca e cor de texto. O #2E8B57 continuou no logo e nas ilustrações, onde não precisa ser lido.
  2. Escureceu o verde para texto até passar com folga. #1F6B43 dá 6,48 sobre branco e 5,94 sobre o cinza claro dos cards, #F5F5F5. O verde original daria só 3,89 sobre os cards.
  3. Usou o verde escuro no fundo do botão. Texto branco sobre #1F6B43 dá os mesmos 6,48.
  4. Refez a conta no tema escuro. O verde original sobre o fundo #121212 dá 4,41 e reprova. O tema escuro ganhou um verde mais claro, #3CB371, que dá 7,03 sobre esse fundo.
  5. 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. --verde vira mentira quando o tema escuro precisa de outro tom; --cor-texto-destaque continua 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?

Avalie este artigo

Referência: http://www.cultura.ufpa.br/dicas/htm/htm-cor2.htm