Bhinish Dhiman

Verificação de contraste no CI: faça o build falhar

Traduzido do original em inglês. Ler o original

Coloque os tokens de cor num teste que confira cada par de texto sobre superfície contra a exigência de contraste dele, e rode esse teste no CI. Assim, uma mudança de token que quebra o contraste faz o build falhar como qualquer outra regressão, em vez de ir para produção e esperar por uma auditoria.

O contraste é verificado quando alguém lembra de verificar. Esse é todo o problema. É uma etapa manual no fim do processo, ela disputa espaço com um prazo, e perde.

A solução é transformá-la numa asserção automatizada, que é como todo outro tipo de regressão é tratado.

Por que a revisão manual falha

Ela acontece tarde demais. O contraste costuma ser revisado perto do fim, quando a cor já sustenta o design e mudá-la significa reabrir decisões já fechadas.

Ela não sobrevive a refatorações. Alguém clareia um cinza em um passo no arquivo de tokens, seis meses depois da auditoria, e nada pega isso.

Ela não acompanha os temas. Claro e escuro dobram a área a cobrir, e o lado escuro é justamente onde o WCAG 2 é menos confiável.

Ninguém reverifica um design system. Um token com contraste insuficiente se espalha por todos os produtos que o consomem.

Cada um desses pontos é a marca de algo que deveria ser um teste.

O que verificar

Não todos os pares possíveis, e sim os pares que você realmente publica. Na maioria dos sistemas, é uma lista curta:

  • Texto do corpo sobre o fundo da página
  • Texto do corpo sobre um card ou superfície elevada
  • Texto atenuado ou secundário sobre os dois
  • Cor de link sobre os dois
  • Rótulo do botão sobre o fundo do botão, para cada variante
  • Cor da borda sobre o fundo ao redor, se a borda identifica um controle

Multiplique pelo número de temas. De doze a vinte asserções cobrem a maioria dos design systems.

Um exemplo completo

import { describe, expect, it } from 'vitest';
import { contrastRatio } from './wcag';

const LIGHT = {
  ink: '#10141c',
  inkSoft: '#2a313d',
  muted: '#5a6473',
  paper: '#f7f8fa',
  card: '#ffffff',
  accent: '#006ad5',
};

const PAIRS = [
  { name: 'body on paper', fg: LIGHT.inkSoft, bg: LIGHT.paper, required: 4.5 },
  { name: 'body on card', fg: LIGHT.inkSoft, bg: LIGHT.card, required: 4.5 },
  { name: 'muted on paper', fg: LIGHT.muted, bg: LIGHT.paper, required: 4.5 },
  { name: 'heading on paper', fg: LIGHT.ink, bg: LIGHT.paper, required: 3 },
  { name: 'button label', fg: '#ffffff', bg: LIGHT.accent, required: 4.5 },
];

describe('design tokens pass contrast', () => {
  for (const p of PAIRS) {
    it(`${p.name} clears ${p.required}:1`, () => {
      const ratio = contrastRatio(p.fg, p.bg);
      expect(ratio, `${p.name}: got ${ratio.toFixed(2)}:1`).toBeGreaterThanOrEqual(p.required);
    });
  }
});

A mensagem de falha importa. expected 4.21 to be greater than or equal to 4.5 manda alguém sair à caça. muted on paper: got 4.21:1 diz exatamente qual token mudar.

Acrescente a asserção APCA para tudo o que é pequeno

O WCAG 2 não enxerga tipografia, então uma razão aprovada num rótulo de 12px não prova nada. Se o que importa para você é a experiência de leitura, e não só o número de conformidade, verifique também o APCA, e cobre limites diferentes de papéis de texto diferentes.

O APCA pede Lc 90 para colunas de texto de corpo e Lc 60 para conteúdo e subtítulos. Aplicar o limite do corpo a um rótulo de 13px o empurraria para quase preto e achataria a sua hierarquia, o que não ajuda ninguém. Verifique o padrão que realmente se aplica a cada papel.

O que encontramos fazendo isso

Acrescentamos isso ao nosso próprio site depois de construir a ferramenta, e esperávamos que passasse. O teste encontrou sete falhas de APCA na interface que passavam no WCAG 2 em todos os casos. A pior era a cor de link do modo escuro, com Lc 37 contra uma exigência de 75: realmente difícil de ler, e invisível para qualquer verificação baseada em razão.

Duas das correções foram tipográficas, e não cromáticas. Os nossos rótulos de 11px não passavam com contraste nenhum, então viraram 13px com peso 500. Os rótulos dos botões não passavam em branco sobre a cor de destaque a 15px, então viraram 17px com peso 600.

Nenhuma dessas é uma correção a que você chegaria ajustando um valor hex.

Calcular a correção em vez de chutar

Quando uma asserção falha, a tentação é escurecer a cor até ela passar. É assim que as paletas vão ficando barrentas com o tempo.

Melhor calcular a mudança mínima: mantenha matiz e croma e procure no eixo da luminosidade o valor mais próximo que passa do limite. Você obtém a menor mudança visual que resolve o problema, e não uma mudança arbitrária, e a cor continua reconhecível.

Geramos todos os valores corrigidos da nossa própria paleta dessa forma, em vez de escolher a olho. Várias das mudanças ficaram abaixo de ΔE 1: imperceptíveis, e ainda assim a diferença entre reprovar e passar.

A parte cultural

O motivo de isso funcionar não é técnico. É que um build que falha é um sinal compartilhado e inequívoco, enquanto uma revisão de acessibilidade é a opinião de uma pessoa disputando com uma data de lançamento.

Quando o contraste vira um teste, ninguém precisa defendê-lo. Ele simplesmente fica vermelho, e o que fica vermelho é corrigido.

O nosso verificador de contraste é a metade interativa disso: quando um teste falha, ele dá a você a cor aprovada mais próxima para colar de volta no arquivo de tokens.