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, e 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-label
etitle
: 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-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');
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 ao clique e adicionamos a lógica do teclado para controlar o foco.
// 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 (0)