Tutorial#wcag#teclado#foco#aria#tutorial

Navegação por teclado: como testar e corrigir no seu site

Guia prático para testar a acessibilidade por teclado e corrigir os problemas mais comuns — foco invisível, elementos inacessíveis, armadilhas em modais. Sem ferramentas especiais, apenas Tab e Enter.

Web para Todos8 min de leitura

O teste de acessibilidade mais rápido que existe cabe em uma linha: feche o mouse, pressione Tab, e tente usar seu site. Se travar em algum ponto, você encontrou um bug que exclui usuários com deficiências motoras, pessoas cegas usando leitor de tela, e qualquer um que prefira o teclado ao mouse.

O que o WCAG exige

Dois critérios fundamentais, ambos de Nível A (o mínimo absoluto):

  • 2.1.1 — Teclado: toda funcionalidade disponível por mouse deve estar disponível via teclado, sem exigir tempo específico para pressionar as teclas
  • 2.4.7 — Foco Visível (AA): todo elemento interativo deve ter indicação visual de foco quando ativo

Uma falha em 2.1.1 é bloqueadora — o site não pode declarar nenhum nível WCAG enquanto tiver elementos inacessíveis por teclado.

O protocolo de teste em 5 minutos

Abra qualquer página do seu site e faça isso:

Passo 1 — Tab sequencial: Pressione Tab repetidamente e verifique:

  • Você consegue ver onde está o foco? (Deve haver um indicador visual em volta do elemento ativo)
  • A ordem de foco faz sentido? (Da esquerda para direita, de cima para baixo, seguindo a leitura visual)
  • Todos os botões, links e campos de formulário são alcançáveis?

Passo 2 — Interações: Em cada elemento interativo, pressione Enter ou Espaço para ativá-lo. Em dropdowns e menus, verifique se Escape fecha e devolve o foco ao ponto de origem.

Passo 3 — Formulários: Preencha um formulário inteiro usando apenas o teclado. Envie com Enter. Funciona?

Passo 4 — Direção reversa: Shift+Tab navega na ordem inversa. Teste isso também.

Se o foco desapareceu, se você ficou preso em algum elemento, ou se alguma funcionalidade é inacessível — você encontrou uma falha real.

Os 5 problemas mais comuns

1. Foco invisível: o erro mais comum

/* ❌ O pior padrão — remove totalmente o indicador de foco */
* { outline: none; }
button:focus { outline: none; }
a:focus { outline: none; }

Esse padrão aparece em muitos resets CSS "modernos" porque o outline padrão do browser é visualmente feio. Mas a solução correta não é remover — é substituir por algo melhor:

/* ✅ Remove o padrão feio e substitui com foco acessível e bonito */
:focus-visible {
  outline: 2px solid #0048B6;
  outline-offset: 2px;
  border-radius: 4px;
}

/* :focus-visible só aparece na navegação por teclado — não aparece ao clicar com o mouse */

Em Tailwind CSS:

<button class="focus-visible:outline-none focus-visible:ring-2
               focus-visible:ring-blue-600 focus-visible:ring-offset-2
               rounded-lg px-4 py-2">
  Enviar
</button>

A pseudo-classe :focus-visible (e focus-visible: no Tailwind) é inteligente: ela só mostra o anel de foco quando o elemento foi ativado por teclado, não por clique do mouse. Isso resolve tanto o problema de acessibilidade quanto a queixa estética.

2. Divs e spans clicáveis que não recebem foco

<!-- ❌ Div com onclick não é focável por teclado — usuário de teclado não consegue ativar -->
<div onclick="abrirModal()">Abrir detalhes</div>

<!-- ❌ Span também não -->
<span onclick="deletarItem(id)">Excluir</span>

<!-- ✅ Sempre use o elemento semântico correto -->
<button type="button" onclick="abrirModal()">Abrir detalhes</button>
<button type="button" onclick="deletarItem(id)">Excluir</button>

O <button> já tem tudo: focável por Tab, ativável por Enter e Espaço, role="button" implícito, e funciona com tecnologias assistivas sem nenhuma configuração extra.

Se por algum motivo você precisar de um elemento não-semântico interativo (ex: integrando uma biblioteca de terceiros), o mínimo necessário é:

<div
  role="button"
  tabindex="0"
  onkeydown="if(event.key==='Enter'||event.key===' ')abrirModal()"
  onclick="abrirModal()"
  aria-label="Abrir detalhes do item"
>
  Abrir detalhes
</div>

Mas prefira sempre o <button> — menos código, mais correto.

3. Armadilha de foco em modais e drawers

Quando um modal abre, o foco deve ir para dentro dele. Quando fecha, deve voltar ao elemento que o abriu. Sem esse comportamento, o usuário de teclado perde completamente a referência de onde está.

// React — padrão correto para modais acessíveis
import { useRef, useEffect } from 'react'

function Modal({
  isOpen,
  onClose,
  triggerRef,
  children,
}: {
  isOpen: boolean
  onClose: () => void
  triggerRef: React.RefObject<HTMLButtonElement>
  children: React.ReactNode
}) {
  const modalRef = useRef<HTMLDivElement>(null)

  // Move o foco para o modal ao abrir
  useEffect(() => {
    if (isOpen) {
      modalRef.current?.focus()
    }
  }, [isOpen])

  // Fecha com Escape e devolve o foco ao trigger
  useEffect(() => {
    if (!isOpen) return
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose()
        triggerRef.current?.focus()
      }
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [isOpen, onClose, triggerRef])

  if (!isOpen) return null

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
      aria-labelledby="modal-title"
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50
                 focus-visible:outline-none"
    >
      <div className="bg-white rounded-2xl p-6 max-w-md w-full shadow-xl">
        <h2 id="modal-title" className="font-bold text-lg text-slate-900 mb-4">
          Título do Modal
        </h2>
        {children}
        <button
          type="button"
          onClick={() => { onClose(); triggerRef.current?.focus() }}
          className="mt-4 text-sm text-gray-500 hover:text-gray-900
                     focus-visible:outline-none focus-visible:ring-2
                     focus-visible:ring-blue-600 rounded"
        >
          Fechar
        </button>
      </div>
    </div>
  )
}

O tabIndex={-1} no container do modal permite mover o foco programaticamente via .focus() sem fazer o elemento aparecer na sequência de Tab do usuário.

4. Skip link ausente

Em páginas com menus de navegação longos, usuários de teclado precisam pressionar Tab dezenas de vezes para chegar ao conteúdo principal. O skip link resolve isso:

<!-- Primeiro elemento do <body>, visualmente oculto mas presente no DOM -->
<a
  href="#main-content"
  class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4
         focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white
         focus:rounded-lg focus:font-semibold focus:shadow-lg"
>
  Ir para o conteúdo principal
</a>

<!-- ... header e navegação ... -->

<main id="main-content" tabindex="-1">
  <!-- O tabIndex="-1" permite receber foco programático -->
</main>

Ao pressionar Tab na primeira vez na página, o link "Ir para o conteúdo principal" aparece. Ao pressionar Enter, o foco pula direto para o <main>.

5. Ordem de foco quebrada por CSS

Layouts flexbox e grid podem reordenar elementos visualmente sem mudar o DOM, criando uma sequência de Tab que não corresponde ao que o usuário vê:

<!-- ❌ CSS reordena visualmente mas Tab segue o DOM — sequência confusa -->
<div style="display:flex; flex-direction:row-reverse">
  <button>Cancelar</button>   <!-- Tab chega aqui por último visualmente -->
  <button>Confirmar</button>  <!-- Tab chega aqui primeiro visualmente -->
</div>

<!-- ✅ Ordem do DOM corresponde à ordem visual -->
<div style="display:flex; flex-direction:row">
  <button>Confirmar</button>
  <button>Cancelar</button>
</div>

Nunca use tabindex com valor positivo (como tabindex="2", tabindex="5") para tentar corrigir a ordem. Isso cria uma sequência de foco global imprevisível e difícil de manter. Reorganize o HTML.

Ferramentas para automatizar a detecção

O teste manual com Tab é insubstituível, mas ferramentas ajudam a identificar problemas antes:

  • Web para Todos — Analisador: detecta automaticamente foco ausente, outline: none sem substituto, e elementos interativos não-focáveis
  • axe DevTools (extensão Chrome/Firefox): audita a página aberta no browser com 100+ regras
  • Lighthouse: aba Accessibility no Chrome DevTools — gratuito, integrado ao browser
  • NVDA + Firefox (Windows): teste real com leitor de tela — gratuito

Resultado

Um site navegável por teclado não é só para pessoas com deficiência. Formulários preenchíveis por Tab, modais que fecham com Escape, e foco sempre visível são qualidade básica de produto — e fazem qualquer usuário mais rápido.

O critério mínimo é objetivo: se você consegue usar o site inteiro sem tocar no mouse, sem ficar preso em nenhum elemento, o 2.1.1 passa.


Critérios WCAG: 2.1.1 Teclado — Nível A | 2.4.7 Foco Visível — Nível AA | 2.4.3 Ordem do Foco — Nível A

Verifique seu site agora: Analisador de acessibilidade gratuito do Web para Todos

Analise a acessibilidade do seu site agora

Gratuito, sem cadastro. Cole uma URL e receba um diagnóstico em segundos.

Abrir analisador