JavaScript e acessibilidade
LidoEsta aula aborda como tornar aplicações JavaScript acessíveis, cobrindo ARIA, gerenciamento de foco e navegação por teclado, além de boas práticas essenciais. Você aprenderá a implementar recursos de acessibilidade de forma prática e progressiva, com exemplos de código e exercícios.
JavaScript é uma ferramenta poderosa para criar interfaces dinâmicas e interativas, mas, sem os devidos cuidados, pode se tornar uma barreira para pessoas com deficiência. A acessibilidade web (a11y) garante que todos os usuários, independentemente de suas capacidades, possam perceber, entender, navegar e interagir com a web. Nesta aula, exploraremos como JavaScript pode ser usado para melhorar a acessibilidade, em vez de prejudicá-la.
Abordaremos três pilares fundamentais: o uso correto de ARIA (Accessible Rich Internet Applications) para melhorar a semântica para leitores de tela, o gerenciamento de foco e o suporte à navegação por teclado, e, por fim, boas práticas gerais para garantir que sua aplicação seja inclusiva. Ao final, você terá conhecimentos práticos para auditar e corrigir problemas de acessibilidade em seus projetos.
ARIA com JS
ARIA (Accessible Rich Internet Applications) é uma especificação do W3C que define atributos HTML para tornar conteúdos dinâmicos e componentes JavaScript acessíveis a tecnologias assistivas, como leitores de tela. Ela não altera o comportamento visual ou funcional do elemento, mas fornece informações semânticas adicionais, como papéis (roles), estados e propriedades.
Com JavaScript, podemos manipular dinamicamente esses atributos ARIA para refletir o estado atual da interface. Por exemplo, em um menu acordeão, devemos atualizar o atributo aria-expanded para indicar se o painel está aberto ou fechado. É crucial usar ARIA com moderação e somente quando não houver uma alternativa HTML nativa. Por exemplo, prefira o elemento <button> a um <div> com role="button".
Exemplo prático: criar um botão que controla a visibilidade de um conteúdo, atualizando o atributo aria-expanded e aria-controls:
const toggleButton = document.getElementById('toggle');
const content = document.getElementById('content');
toggleButton.addEventListener('click', () => {
const isExpanded = toggleButton.getAttribute('aria-expanded') === 'true';
toggleButton.setAttribute('aria-expanded', !isExpanded);
content.hidden = isExpanded; // usando hidden para ocultar, mas pode usar classe
});No código acima, o atributo aria-expanded é atualizado a cada clique, e o conteúdo é mostrado/ocultado. Isso informa ao leitor de tela se o conteúdo está visível ou não. Lembre-se de que o atributo hidden é uma forma semântica de ocultar conteúdo, mas você também pode usar display: none via CSS; o importante é que o leitor de tela respeite a ocultação.
Outro exemplo comum é o uso de aria-live para anunciar mudanças dinâmicas, como atualizações de carrinho de compras ou mensagens de erro. Podemos criar uma região viva e inserir conteúdo nela via JavaScript:
const liveRegion = document.getElementById('live-region');
function announce(message) {
liveRegion.textContent = message;
}
// Chamar announce('Item adicionado ao carrinho') quando necessárioNo HTML, a região deve ter aria-live="polite" (ou assertive para urgência) e, inicialmente, estar vazia. Isso permite que leitores de tela anunciem a mensagem sem interromper o usuário de forma brusca.
Foco e teclado
O gerenciamento de foco é essencial para usuários que navegam pelo teclado, incluindo pessoas com mobilidade reduzida. Quando criamos componentes dinâmicos, como modais, menus suspensos ou carrosséis, devemos garantir que o foco seja movido corretamente e que o usuário possa operar tudo com o teclado.
Um dos problemas mais comuns é o foco ficar preso em um modal ou em um elemento que não pode ser fechado. A técnica de "armadilha de foco" (focus trap) impede que o Tab saia do modal enquanto ele estiver aberto. Vamos ver como implementar uma armadilha de foco em um modal simples:
const modal = document.getElementById('modal');
const openBtn = document.getElementById('open-modal');
const closeBtn = document.getElementById('close-modal');
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
function trapFocus(event) {
const focusable = modal.querySelectorAll(focusableElements);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
openBtn.addEventListener('click', () => {
modal.hidden = false;
closeBtn.focus();
document.addEventListener('keydown', trapFocus);
});
closeBtn.addEventListener('click', () => {
modal.hidden = true;
openBtn.focus();
document.removeEventListener('keydown', trapFocus);
});Além disso, é importante restaurar o foco ao elemento que abriu o modal, como fizemos acima. Isso mantém a continuidade da navegação.
Também devemos garantir que todos os elementos interativos sejam acessíveis por teclado. Elementos como <div> com tabindex="0" podem ser focados, mas é preferível usar elementos nativos como <button> e <a>. Se você precisar criar um componente personalizado, use tabindex e implemente os eventos de teclado apropriados, como Enter e Espaço para ativação.
Outra técnica é o uso de atalhos de teclado. Por exemplo, em um menu, permitir que as setas naveguem entre itens. Isso requer gerenciar o foco nos itens do menu. Aqui está um exemplo parcial:
const menuItems = document.querySelectorAll('.menu-item');
let currentIndex = 0;
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
currentIndex = (index + 1) % menuItems.length;
menuItems[currentIndex].focus();
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
currentIndex = (index - 1 + menuItems.length) % menuItems.length;
menuItems[currentIndex].focus();
}
});
});Lembre-se de que a implementação de padrões de teclado deve seguir as diretrizes WAI-ARIA Authoring Practices para garantir consistência.
Boas práticas
Além de ARIA e foco, existem várias boas práticas para garantir que seu código JavaScript seja acessível. Primeiro, sempre que possível, use HTML semântico e aprimore com JavaScript, em vez de substituir elementos nativos por divs e spans. Isso reduz a necessidade de ARIA e melhora a compatibilidade.
Segundo, forneça alternativas para conteúdo não textual, como imagens (atributo alt) e ícones (texto ou aria-label). Em JavaScript, você pode adicionar dinamicamente esses atributos, mas certifique-se de que eles estejam presentes.
Terceiro, teste sua aplicação com leitores de tela e navegação por teclado. Use ferramentas como o Lighthouse do Chrome, axe ou WAVE para identificar problemas. Automatize testes de acessibilidade em seu pipeline de CI/CD.
Quarto, evite alterar o foco de forma inesperada. Por exemplo, não mova o foco para um elemento que não é relevante para a ação do usuário. Se uma página carregar e o foco for movido automaticamente, pode confundir usuários de leitor de tela.
Quinto, preste atenção à ordem de leitura. O conteúdo dinâmico deve ser inserido em uma ordem lógica no DOM. Use aria-live para anunciar mudanças, mas não exagere, pois muitas interrupções podem ser irritantes.
Por fim, lembre-se de que acessibilidade é um processo contínuo. Envolva pessoas com deficiência nos testes e mantenha-se atualizado com as diretrizes WCAG e WAI-ARIA.
Referências
- MDN: ARIA
- WAI-ARIA Authoring Practices
- WAI-ARIA Specification (W3C)
- MDN: Keyboard-navigable JavaScript widgets
- web.dev: Learn Accessibility
- WCAG 2.1 Quick Reference
- Lighthouse
Exercícios
- Implemente um botão que controla a visibilidade de um painel usando
aria-expandedearia-controls. O painel deve ser ocultado com o atributohiddene o atributoaria-expandeddeve ser atualizado corretamente.✓ Resposta:const button = document.getElementById('toggle'); const panel = document.getElementById('panel'); button.addEventListener('click', () => { const isExpanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', !isExpanded); panel.hidden = isExpanded; });No HTML, o botão deve ter
aria-expanded="false"earia-controls="panel". - Crie uma região
aria-live="polite"e use JavaScript para anunciar uma mensagem quando um formulário for enviado com sucesso (simule com um evento de clique).✓ Resposta:const liveRegion = document.getElementById('live-region'); const form = document.getElementById('my-form'); form.addEventListener('submit', (e) => { e.preventDefault(); liveRegion.textContent = 'Formulário enviado com sucesso!'; });No HTML:
<div id="live-region" aria-live="polite"></div> - Implemente uma armadilha de foco para um modal. O modal deve conter um botão de fechar e um link. Ao abrir, o foco deve ir para o botão de fechar. Ao pressionar Tab, o foco deve circular entre os elementos do modal, sem sair.✓ Resposta:
const modal = document.getElementById('modal'); const openBtn = document.getElementById('open'); const closeBtn = document.getElementById('close'); const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; function trapFocus(e) { const focusable = modal.querySelectorAll(focusableElements); const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } } openBtn.addEventListener('click', () => { modal.hidden = false; closeBtn.focus(); document.addEventListener('keydown', trapFocus); }); closeBtn.addEventListener('click', () => { modal.hidden = true; openBtn.focus(); document.removeEventListener('keydown', trapFocus); }); - Crie uma lista de itens de menu que possa ser navegada com as setas do teclado. Use a rotação ao chegar ao final. Destaque os itens com
tabindex="0"e gerencie o foco.✓ Resposta:const items = document.querySelectorAll('.menu-item'); let current = 0; items.forEach((item, index) => { item.setAttribute('tabindex', index === 0 ? '0' : '-1'); item.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); current = (index + 1) % items.length; items[current].focus(); } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); current = (index - 1 + items.length) % items.length; items[current].focus(); } }); }); - Escreva um trecho de código que adicione dinamicamente um atributo
aria-labela um ícone de botão (um elemento<i>dentro de um<button>) para que ele seja anunciado corretamente.✓ Resposta:const iconButton = document.querySelector('.icon-button'); iconButton.setAttribute('aria-label', 'Fechar'); // Ou, se o ícone for filho: const icon = iconButton.querySelector('i'); icon.setAttribute('aria-hidden', 'true'); iconButton.setAttribute('aria-label', 'Fechar');
Observações finais
Acessibilidade não é um recurso opcional; é uma exigência para criar produtos inclusivos. Com JavaScript, temos o poder de melhorar ou piorar a experiência de usuários com deficiência. Sempre teste suas implementações com tecnologias assistivas e siga as diretrizes estabelecidas.
Lembre-se de que a acessibilidade também beneficia usuários sem deficiência, como aqueles que preferem navegação por teclado ou estão em ambientes com restrições. Invista tempo em aprender e aplicar esses conceitos, e seus projetos serão mais robustos e universais.