Programação Por Equipe DevBras

Capa do artigo: Interruptor só com CSS que funciona no teclado e no leitor de tela

Interruptor só com CSS que funciona no teclado e no leitor de tela

Monte um interruptor liga/desliga a partir de um checkbox real, sem JavaScript, mantendo foco visível, rótulo clicável e estado anunciado pelo leitor de tela.

A tela de preferências ganhou interruptores bonitos, feitos só com CSS a partir de checkboxes. No teste com o teclado, o Tab passa por eles sem mostrar nada, ou pula direto para o botão Salvar. O motivo é uma linha: display: none no input. A resposta curta: esconda a aparência do checkbox, nunca o checkbox. O input continua na página, recebe o foco, tem um rótulo ligado por for e mostra um anel de foco visível no desenho do interruptor. Feito assim, o componente funciona com mouse, teclado, toque e leitor de tela sem uma linha de JavaScript.

Componente só com CSS é uma ótima escolha justamente porque herda o comportamento do controle nativo. O erro é jogar fora essa herança no momento de estilizar.

Por que o interruptor some do teclado

Um checkbox nativo traz de graça tudo o que dá trabalho fazer à mão: entra na ordem do Tab, alterna com a tecla Espaço, é enviado no formulário, aparece para o leitor de tela como "caixa de seleção, marcada" ou "não marcada" e fica clicável pelo rótulo. Tudo isso depende de o elemento existir na árvore de acessibilidade.

display: none e visibility: hidden removem o elemento dessa árvore e da ordem de foco. O rótulo ainda alterna o estado quando clicado com o mouse, e por isso o componente parece funcionar no teste rápido. Para o teclado ele deixou de existir. A outra forma de quebrar é mais sutil: o input fica, mas alguém escreve outline: none para tirar o "contorno feio" e não coloca nada no lugar. O foco está lá, só que ninguém vê onde.

O terceiro problema é o estado comunicado apenas por cor. Verde para ligado e cinza para desligado falha para quem não distingue essas cores e falha no modo de alto contraste do Windows, em que o navegador substitui as cores de fundo definidas pela página.

Como montar o interruptor a partir do checkbox

O exemplo é a preferência "Receber notificações por e-mail". A técnica usada aqui estiliza o próprio input com appearance: none, então o elemento que recebe o foco é o mesmo que você desenha. Não há input escondido nem pseudo-elemento no rótulo para sincronizar.

  1. Comece pelo HTML nativo: um input type="checkbox" com id, name e um label com for apontando para ele.
  2. Anuncie como interruptor: role="switch" é permitido em checkbox e faz leitores de tela compatíveis anunciarem "ligado" e "desligado". Sem ele, o anúncio de caixa marcada continua correto.
  3. Remova só a aparência: appearance: none tira o desenho padrão e mantém foco, clique e envio no formulário.
  4. Desenhe trilho e botão: o trilho é o próprio input e o botão é um pseudo-elemento que se desloca quando o estado é :checked.
  5. Mostre o foco: :focus-visible com um contorno de pelo menos 2 px e cor com bom contraste contra o fundo.
  6. Diferencie o estado além da cor: a posição do botão muda, e a borda continua visível nos dois estados.

O HTML

<div class="preferencia">
  <input type="checkbox" id="notificacoes" name="notificacoes"
         class="interruptor" role="switch" checked>
  <label for="notificacoes">Receber notificações por e-mail</label>
</div>

O CSS

.interruptor {
  appearance: none;
  position: relative;
  width: 2.75rem;
  height: 1.5rem;
  margin: 0 0.5rem 0 0;
  border: 2px solid #555;
  border-radius: 999px;
  background: #fff;
  cursor: pointer;
  vertical-align: middle;
}

.interruptor::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: #555;
  transition: transform 0.15s;
}

.interruptor:checked {
  background: #0b6e4f;
  border-color: #0b6e4f;
}

.interruptor:checked::before {
  background: #fff;
  transform: translateX(1.25rem);
}

.interruptor:focus-visible {
  outline: 3px solid #1a5fb4;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .interruptor::before {
    transition: none;
  }
}

As medidas fecham: o trilho tem 2,75 rem por dentro da borda, o botão tem 1,25 rem e começa a 2 px da esquerda, e o deslocamento de 1,25 rem o leva até 2 px da direita. Se mudar uma medida, refaça essa conta, senão o botão encosta ou sobra espaço de um lado só. Pseudo-elemento em checkbox com appearance: none funciona nos navegadores atuais, mas vale conferir nos que o seu público usa.

Quando você precisa esconder o input

Alguns designs pedem o desenho no rótulo, por exemplo um interruptor grande com ícones. Nesse caso, o input sai da vista com a técnica de "visualmente escondido" (posição absoluta, 1 px de tamanho, clip-path ou clip, overflow: hidden), nunca com display: none. E o anel de foco vai para o desenho com o seletor de irmão: .interruptor:focus-visible + label::before. O guia sobre esconder o botão de escolher arquivo sem esconder o teclado usa a mesma técnica num input de arquivo.

Como testar em dois minutos

Não precisa de ferramenta paga. Faça quatro testes, nesta ordem, sempre com a mesma preferência de notificações:

  1. Aperte Tab a partir do topo da página. O anel de foco tem de aparecer no interruptor, não em algum lugar invisível.
  2. Com o foco nele, aperte Espaço. O estado muda e o botão se desloca.
  3. Clique no texto "Receber notificações por e-mail". O estado muda também.
  4. Ligue o leitor de tela do sistema (Narrador no Windows, VoiceOver no macOS) e chegue ao controle. Ele precisa ler o rótulo e o estado.

Repita o primeiro teste no tema escuro, se o site tiver um. Contorno azul-escuro some em fundo escuro, um problema detalhado no post sobre anel de foco que some no tema escuro.

Erros comuns

  • display: none ou visibility: hidden no input: o controle sai da ordem do Tab e do leitor de tela, e só o mouse consegue alternar.
  • outline: none sem substituto: o foco existe, mas quem navega pelo teclado não sabe onde está e aciona o controle errado.
  • Estado só pela cor: no alto contraste do Windows ou para quem não distingue verde de cinza, ligado e desligado ficam iguais.
  • Div com clique em vez de checkbox: perde o envio no formulário, o Espaço, o anúncio de estado e o rótulo clicável, e você passa a reescrever tudo em JavaScript.
  • Rótulo sem for ou com id errado: o leitor de tela anuncia um controle sem nome e clicar no texto não faz nada.
  • Interruptor que aplica a mudança na hora sem dizer: se ligar já salva, mostre uma confirmação em texto. Se depende do botão Salvar, não sugira o contrário.

Checklist

  • O controle é um input type="checkbox" real, com name e id.
  • Existe um label com for igual ao id do input.
  • Nenhuma regra aplica display: none ou visibility: hidden no input.
  • O foco por teclado mostra um contorno visível, também no tema escuro.
  • Espaço alterna o estado quando o controle está focado.
  • Ligado e desligado se distinguem pela posição, não só pela cor.
  • O formulário envia o valor do checkbox quando ele está marcado.

Abra a página de configurações do seu sistema, ponha o mouse de lado e tente desligar uma notificação usando só Tab e Espaço. Você consegue ver onde o foco está o tempo todo? Se não consegue, o CSS limpou demais.

Avalie este artigo

Referência: http://tableless.com.br/elementos-de-interface-utilizando-apenas-css3/