DEV Community

Cover image for Como criar um menu hambúrguer acessível com a "armadilha de foco" (Focus Trap)
Carolina Gonçalves
Carolina Gonçalves

Posted on • Edited on

Como criar um menu hambúrguer acessível com a "armadilha de foco" (Focus Trap)

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> 
Enter fullscreen mode Exit fullscreen mode

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) ou true (aberto).
  • aria-label e title: 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> 
Enter fullscreen mode Exit fullscreen mode

O essencial aqui:

  • <nav> com id: A tag <nav> é a correta para um bloco de links principal. O id é o "alvo" do aria-controls do 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'); 
Enter fullscreen mode Exit fullscreen mode

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'; }; 
Enter fullscreen mode Exit fullscreen mode

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(); }); }); 
Enter fullscreen mode Exit fullscreen mode

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(); } }); }); 
Enter fullscreen mode Exit fullscreen mode

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).

GIF animado mostrando a navegação por teclado em um menu mobile. O foco do teclado circula entre o botão de menu e os links internos, sem escapar para o conteúdo da página, demonstrando a 'armadilha de foco'. Demonstração do menu acessível com a 'Armadilha de Foco' em funcionamento.


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)

Collapse
 
alexdlli profile image
Alex Sandro

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 ^^