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
- Crie uma variável só para o foco. Uma
--focono:root, redefinida no tema escuro. Assim o tema nunca esquece dela. - 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ó. - 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. - Dê espessura e distância. Anel de 1 pixel some em tela de alta densidade. Use pelo menos 2 pixels e um
outline-offsetpara descolar do componente, de modo que o anel seja comparado com o fundo e não com a borda do botão. - 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 umoutline, mesmo que transparente, porque o navegador pinta o outline com a cor do sistema nesse modo. - 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: nonesem 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-visiblepor: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
--focoredefinida no tema escuro e noprefers-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 eoutline-offset. - Nenhum
outline: noneficou 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á?
Referência: https://developer.mozilla.org/docs/Web/CSS/:focus-visible