Programação Por Equipe DevBras

Capa do artigo: Anel de foco que some no tema escuro: como deixar o Tab visível nos dois temas

Anel de foco que some no tema escuro: como deixar o Tab visível nos dois temas

Desenhe um indicador de foco com :focus-visible que tenha contraste no tema claro e no escuro, sem outline: none órfão, e teste com Tab nos dois fundos.

O painel financeiro ganhou tema escuro numa sexta. Na segunda, o Paulo, que trabalha só com teclado por causa de uma lesão no punho, apertou Tab cinco vezes e não fazia ideia de qual botão estava selecionado. Ele apertou Enter no escuro, literalmente, e emitiu um boleto que não queria. O anel de foco existia. Era azul #0b57d0 sobre um card #1e1e1e, com contraste de 2,6 para 1. Na prática, invisível.

A resposta curta: o indicador de foco precisa de uma cor própria para cada tema, com contraste de pelo menos 3:1 contra o fundo ao redor, aplicada com :focus-visible. Nunca remova o outline sem colocar outro indicador no lugar. E teste com Tab nos dois temas, não só no que o designer aprovou.

Por que o foco desaparece quando o tema muda

O anel de foco é a única pista que quem navega por teclado tem de onde está. Mouse tem cursor; teclado tem o foco. Quando ele some, a pessoa precisa adivinhar, e adivinhar num sistema que emite boleto, apaga registro ou envia e-mail é perigoso.

O problema no tema escuro tem três origens, quase sempre juntas. Primeiro, a cor do foco foi escolhida para fundo branco e nunca revisada. O azul médio que tem 6,4 para 1 contra o branco cai para menos de 3 contra cinza-carvão. Segundo, alguém escreveu outline: none num reset antigo porque o anel "ficava feio no clique", e o substituto foi uma sombra suave que só aparece no fundo claro. Terceiro, o tema escuro foi feito trocando variáveis de cor de texto e fundo, mas a variável do foco não existia, então o valor ficou fixo no CSS do componente.

As diretrizes WCAG pedem que o foco do teclado seja visível e que componentes de interface tenham contraste de 3:1 com o que está ao redor. Esse número é a régua prática: se o anel não passa de 3:1 contra o fundo onde ele é desenhado, trate como bug.

Como desenhar um foco que funciona nos dois temas

  1. Crie uma variável só para o foco. Uma --foco no :root, redefinida no tema escuro. Assim o tema nunca esquece dela.
  2. Escolha as cores medindo, não olhando. No painel, o claro usa #0b57d0 (6,4:1 contra branco) e o escuro usa #8ab4f8 (7,9:1 contra #1e1e1e). O azul claro sobre branco daria só 2,1, por isso não dá para usar uma cor só.
  3. Use :focus-visible. O navegador mostra o indicador quando a pessoa chega pelo teclado e, na maioria dos casos, não mostra quando ela clica num botão com o mouse. Campos de texto mostram nos dois casos, o que é correto, porque quem clica num campo precisa ver onde vai digitar.
  4. Dê espessura e distância. Anel de 1 pixel some em tela de alta densidade. Use pelo menos 2 pixels e um outline-offset para descolar do componente, de modo que o anel seja comparado com o fundo e não com a borda do botão.
  5. Cubra o modo de alto contraste. No modo de cores forçadas do Windows, box-shadow é removido. Se o seu foco for só sombra, ele some. Mantenha um outline, mesmo que transparente, porque o navegador pinta o outline com a cor do sistema nesse modo.
  6. Teste componente por componente. Link, botão primário, botão secundário, campo, checkbox, item de menu e card clicável. Cada um pode ter fundo diferente.

O CSS do painel

:root {
  --fundo: #ffffff;
  --card: #f5f5f5;
  --texto: #1a1a1a;
  --foco: #0b57d0;
}

[data-tema="escuro"] {
  --fundo: #121212;
  --card: #1e1e1e;
  --texto: #f1f1f1;
  --foco: #8ab4f8;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-tema="claro"]) {
    --fundo: #121212;
    --card: #1e1e1e;
    --texto: #f1f1f1;
    --foco: #8ab4f8;
  }
}

:focus-visible {
  outline: 3px solid var(--foco);
  outline-offset: 2px;
}

/* Botão com fundo da mesma cor do foco: anel duplo */
.botao-primario:focus-visible {
  outline: 3px solid var(--foco);
  outline-offset: 2px;
  box-shadow: 0 0 0 2px var(--fundo);
}

O bloco com prefers-color-scheme cuida de quem nunca escolheu tema no painel e usa o sistema no escuro. O atributo data-tema cuida de quem escolheu manualmente. Sem as duas coisas, você corre o risco de mostrar fundo escuro com foco de tema claro.

O caso do botão azul com foco azul

O botão "Emitir boleto" é azul. No tema claro, o anel azul encostado num botão azul vira uma borda mais grossa, e a pessoa não percebe a diferença entre focado e não focado. O outline-offset resolve parte, porque abre um espaço. A sombra de 2 pixels na cor do fundo, desenhada entre o botão e o anel, garante que exista uma faixa de contraste separando os dois. Em cores forçadas a sombra some, mas o outline continua.

O que fazer com o reset que tira o outline

Procure no projeto por outline: none e outline: 0. Cada ocorrência precisa de uma de duas respostas: ou ela está dentro de :focus:not(:focus-visible) (esconde só no clique, o que é aceitável), ou existe um substituto visível na mesma regra. Se não houver nenhuma das duas, apague a linha.

grep -rn "outline: *\(none\|0\)" src/

Como testar em cinco minutos

Abra a tela mais usada no tema claro, clique na barra de endereço e aperte Tab até o fim da página. Em cada parada, você precisa saber onde está sem procurar. Troque para o tema escuro e repita. Depois ligue o modo de alto contraste do sistema e repita mais uma vez.

Para medir, use o seletor de cor do DevTools sobre o anel e sobre o fundo ao lado dele e confira a razão em qualquer calculadora de contraste. O post sobre contraste de cor no CSS explica como fazer essa conta. Se o foco cair atrás de um cabeçalho fixo, o problema é outro, tratado em menu fixo que cobre o título e o foco. E se o foco se perde ao fechar um modal, veja modal que devolve o foco para quem abriu.

Erros comuns

  • Uma cor de foco para os dois temas. Funciona bem em um e some no outro. O azul que passa no claro reprova no escuro, e vice-versa.
  • outline: none sem substituto. Quem usa teclado perde a única referência de posição e passa a acionar coisas por tentativa.
  • Foco feito só com box-shadow. No modo de cores forçadas do Windows a sombra é descartada e o indicador desaparece justamente para quem mais precisa.
  • Trocar :focus-visible por :hover. O estilo aparece para o mouse e nunca para o teclado, que não dispara hover.
  • Medir o contraste contra a cor errada. O anel com outline-offset é visto contra o fundo da página ou do card, não contra o botão. Medir contra o botão dá um número que não corresponde ao que a pessoa vê.
  • Indicador só de mudança de cor do texto. Escurecer levemente o texto de um link não é foco visível; a diferença é pequena demais para quem enxerga pouco.

Checklist

  • Existe uma variável --foco redefinida no tema escuro e no prefers-color-scheme: dark.
  • A cor do foco tem pelo menos 3:1 contra o fundo em cada tema, medido com ferramenta.
  • O indicador usa :focus-visible, tem 2 pixels ou mais e outline-offset.
  • Nenhum outline: none ficou sem substituto visível.
  • Botões da mesma cor do foco têm separação (anel duplo ou espaçamento).
  • O foco aparece no modo de alto contraste do Windows.
  • A navegação completa por Tab foi feita no claro, no escuro e em cores forçadas.

Mude agora a sua aplicação para o tema escuro, aperte Tab três vezes na tela principal e responda sem mexer no mouse: em qual elemento o foco está?

Avalie este artigo

Referência: https://developer.mozilla.org/docs/Web/CSS/:focus-visible