Olá, pessoal!
Vamos direto ao ponto: seu menu hambúrguer provavelmente está quebrado para usuários de leitores de tela.
Eu descobri isso da pior maneira. Em 2022, enquanto desenvolvia o site de um evento sobre acessibilidade digital, o designer responsável me fez a seguinte pergunta:
“Você já testou o layout com leitor de tela? O evento é sobre acessibilidade, precisamos dar o exemplo...”
Fui testar no mobile e encontrei o problema: o leitor de tela lia todos os links de um menu visualmente fechado. A pessoa ouvia “Contato, link”, pressionava o "Enter" e nada acontecia. Era um link fantasma.
Tomei aquele "puxão de orelha" e aprendi uma lição: a experiência do usuário tem que ser a mesma para todos, esconder um menu só com CSS não resolve o problema. Era preciso gerenciar o estado (aberto/fechado) e, principalmente, o foco do teclado.
E a solução que encontrei foi implementar a "Focus Trap" (armadilha de foco) usando JavaScript e atributos ARIA. Vou te mostrar o passo a passo:
(Para quem já quiser ver na prática, acesse o resultado final e o projeto completo no CodeSandbox.)
A estrutura HTML do menu
Tudo começa com um HTML semântico dividido em duas partes: o botão que dispara a ação e o painel que guarda os links.
1. O botão de ativação
É onde o usuário clica para abrir o menu. E a dúvida mais comum nessa parte é: por que o leitor de tela precisa ouvir “botão” e não “link”?
Pense assim:
- Um link é como uma porta: leva o usuário pra outro lugar.
- Um botão é como um interruptor: executa uma ação onde ele já está.
Se o leitor de tela anuncia “Menu, link”, a experiência fica confusa. Por isso usamos role="button", que comunica ao leitor de tela que o elemento executa uma ação local.
Agora, talvez você se pergunte: por que usar <a> e não <button> de vez?
A resposta é simples: a tag <a> com href serve como plano B. Se o JavaScript falhar (erro, conexão ruim, bloqueio por extensão), o href ainda leva o usuário até a seção do menu. Um <button> puro não funcionaria.
<a id="pageHeaderHamburgerIcon" href="#pageContainerMainNavMobile" role="button" aria-controls="navigation" aria-expanded="false" aria-label="Abrir menu de navegação" title="Abrir menu de navegação"> ☰<span class="sr-only">Abrir menu</span> </a> Resumindo a função de cada um:
-
role="button": Ajusta a expectativa do leitor de tela, anunciando “botão”. -
href: Garante o fallback se o JavaScript falhar. -
id: Identifica o botão para que o JavaScript possa manipulá-lo. -
aria-controls: Conecta o botão ao menu que ele controla. -
aria-expanded: Indica o estado atual,false(fechado) outrue(aberto). -
aria-labeletitle: Garantem que a função do botão seja sempre clara. Quando o menu está aberto, o JavaScript atualiza o texto para "Fechar menu", e vice-versa, mantendo o usuário sempre informado sobre a próxima ação.
2. O painel de navegação
É o bloco que guarda os links e que aparece e desaparece.
<nav id="pageContainerMainNavMobile"> <h2 class="sr-only" id="mainNavigationLabelMobile">Menu Principal</h2> <ul id="mainNavigationMobile"> <li><a href="#" title="Saiba mais sobre o festival">Sobre</a></li> <li><a href="#" title="Veja quem realiza o evento">Realização</a></li> <li><a href="#" title="Entre em contato conosco">Contato</a></li> </ul> </nav> O essencial aqui:
-
<nav>comid: A tag<nav>é a correta para um bloco de links principal. Oidé o "alvo" doaria-controlsdo botão. -
<h2 class="sr-only">: Este título invisível é uma prática recomendada, pois o leitor de tela o utiliza para dizer ao usuário onde ele está antes de listar as opções de navegação.
Onde a mágica acontece: o JavaScript
O JavaScript é o nosso mestre da bateria: ele dá o ritmo, controla o abre/fecha, atualiza os atributos ARIA e, o mais importante, gerencia o foco do teclado.
Vamos ver a lógica passo a passo:
1. Preparando o terreno
Antes de qualquer ação, nosso script precisa "conhecer" os elementos do HTML. Por isso, começamos selecionando o botão, o menu e o último link em variáveis para usarmos depois.
document.addEventListener('DOMContentLoaded', function() { const keys = { tab: 9, esc: 27, }; const menuButton = document.getElementById('pageHeaderHamburgerIcon'); const navMenu = document.getElementById('pageContainerMainNavMobile'); const menuLinks = navMenu.querySelectorAll('a'); const lastLink = menuLinks[menuLinks.length - 1]; document.body.classList.add('js'); 2. As funções openMenu e closeMenu
Estas são as duas funções que controlam o estado do menu, atualizando os atributos e controlando a visibilidade.
const openMenu = () => { // Atualiza os atributos para o estado "Aberto" menuButton.setAttribute('aria-expanded', 'true'); menuButton.setAttribute('aria-label', 'Fechar menu de navegação'); menuButton.setAttribute('title', 'Fechar menu de navegação'); // Troca o ícone e mostra o menu menuButton.innerHTML = '\u00D7<span class="sr-only">Fechar menu</span>'; navMenu.style.display = 'block'; }; const closeMenu = () => { // Atualiza os atributos para o estado "Fechado" menuButton.setAttribute('aria-expanded', 'false'); menuButton.setAttribute('aria-label', 'Abrir menu de navegação'); menuButton.setAttribute('title', 'Abrir menu de navegação'); // Troca o ícone e esconde o menu menuButton.innerHTML = '\u2630<span class="sr-only">Abrir menu</span>'; navMenu.style.display = 'none'; }; 3. O gatilho e a armadilha de foco
Agora conectamos as funções aos eventos do usuário (clique e teclado) para controlar o foco.
Primeiro, fazemos o menu fechar automaticamente ao clicar em um link:
// Fecha o menu ao clicar em um dos seus links menuLinks.forEach(link => { link.addEventListener('click', () => { closeMenu(); }); }); Em seguida, configuramos o gatilho do menu e a navegação por teclado:
// Gatilho do clique no botão menuButton.addEventListener('click', (e) => { e.preventDefault(); const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'; isExpanded ? closeMenu() : openMenu(); }); // Tecla ESC fecha o menu navMenu.querySelectorAll('*').forEach(el => { el.addEventListener('keydown', (e) => { if (e.keyCode === keys.esc) { closeMenu(); menuButton.focus(); } }); }); // Tab no último item volta para o botão (o loop do foco) lastLink.addEventListener('keydown', (e) => { if (e.keyCode === keys.tab && !e.shiftKey) { e.preventDefault(); menuButton.focus(); } }); // Shift+Tab no botão (com menu aberto) vai para o último item menuButton.addEventListener('keydown', (e) => { const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'; if (isExpanded && e.keyCode === keys.tab && e.shiftKey) { e.preventDefault(); lastLink.focus(); } }); }); Resultado
O menu agora é funcional para todos, visível e navegável por teclado. Teste com leitores de tela (NVDA, VoiceOver) e navegação por teclado (Tab, Shift+Tab, Esc).
Demonstração do menu acessível com a 'Armadilha de Foco' em funcionamento.
- Resultado final: Acesse a demo
- Projeto completo: Acesse o código no CodeSandbox
Conclusão
Uma coisa que eu sempre falo: só podemos dizer que um site seguiu as boas práticas quando ele é acessível ao maior número de pessoas possível.
Da próxima vez que você for codificar um menu, um modal ou um simples botão, faça aquela pergunta: "Eu testei com leitor de tela?".
A diferença na experiência do usuário é enorme.
Top comments (1)
Testar como se fossemos o usuário final é sempre importante, ainda mais quando conseguimos enter os usuários que usam recursos assistivos. Parabéns pelo artigo ^^