12. Acessibilidade
Este documento apresenta práticas e diretrizes para garantir que soluções web desenvolvidas com Astro 5 sejam acessíveis para todos os usuários, incluindo pessoas com deficiências.
Princípios de Acessibilidade Web
WCAG 2.1
As Diretrizes de Acessibilidade para Conteúdo Web (WCAG) 2.1 são organizadas em quatro princípios fundamentais:
- Perceptível: As informações e os componentes da interface do usuário devem ser apresentados de maneira que possam ser percebidos.
- Operável: Os componentes de interface e a navegação devem ser operáveis.
- Compreensível: As informações e a operação da interface do usuário devem ser compreensíveis.
- Robusto: O conteúdo deve ser robusto o suficiente para ser interpretado de forma confiável por uma ampla variedade de agentes de usuário, incluindo tecnologias assistivas.
Níveis de Conformidade
O WCAG 2.1 define três níveis de conformidade:
- Nível A: Nível mínimo de acessibilidade
- Nível AA: Nível intermediário, recomendado para a maioria dos sites
- Nível AAA: Nível mais alto, ideal para sites com públicos específicos
Para a maioria dos projetos, recomendamos alcançar pelo menos o nível AA.
Implementação de Acessibilidade
Semântica HTML
Use elementos HTML semânticos para fornecer significado e estrutura ao conteúdo:
<!-- ❌ Não semântico --><div class="header"> <div class="logo">Logo</div> <div class="nav"> <div class="nav-item">Home</div> <div class="nav-item">Sobre</div> </div></div>
<!-- ✅ Semântico --><header> <div class="logo" role="img" aria-label="Logo da empresa">Logo</div> <nav aria-label="Menu principal"> <ul> <li><a href="/">Home</a></li> <li><a href="/sobre">Sobre</a></li> </ul> </nav></header>Estrutura de Cabeçalhos
Use cabeçalhos (<h1> a <h6>) de forma hierárquica para estruturar o conteúdo:
<!-- ❌ Estrutura incorreta --><h1>Título da Página</h1><h3>Subtítulo</h3> <!-- Pula h2 --><h6>Seção menor</h6> <!-- Pula h4 e h5 -->
<!-- ✅ Estrutura correta --><h1>Título da Página</h1><h2>Subtítulo</h2><h3>Seção menor</h3>Texto Alternativo para Imagens
Forneça texto alternativo para imagens:
<!-- ❌ Sem texto alternativo --><img src="grafico-vendas.png">
<!-- ✅ Com texto alternativo --><img src="grafico-vendas.png" alt="Gráfico mostrando aumento de 30% nas vendas no último trimestre">
<!-- ✅ Imagem decorativa --><img src="decorativa.png" alt="" role="presentation">Contraste de Cores
Garanta contraste suficiente entre texto e fundo:
/* ❌ Contraste insuficiente */.text-light { color: #aaaaaa; /* Cinza claro */ background-color: #ffffff; /* Branco */}
/* ✅ Contraste adequado */.text-dark { color: #333333; /* Cinza escuro */ background-color: #ffffff; /* Branco */}Ferramentas para verificar contraste:
Formulários Acessíveis
Crie formulários com rótulos, agrupamento e mensagens de erro adequados:
<!-- ❌ Formulário inacessível --><form> <input type="text" placeholder="Nome"> <input type="email" placeholder="Email"> <button>Enviar</button></form>
<!-- ✅ Formulário acessível --><form aria-labelledby="form-title"> <h2 id="form-title">Formulário de Contato</h2>
<div class="form-group"> <label for="name">Nome</label> <input type="text" id="name" name="name" aria-required="true" aria-describedby="name-error" > <div id="name-error" class="error-message" aria-live="polite"></div> </div>
<div class="form-group"> <label for="email">Email</label> <input type="email" id="email" name="email" aria-required="true" aria-describedby="email-error" > <div id="email-error" class="error-message" aria-live="polite"></div> </div>
<button type="submit">Enviar</button></form>JavaScript para validação acessível:
document.querySelector('form').addEventListener('submit', (event) => { event.preventDefault();
const nameInput = document.getElementById('name'); const nameError = document.getElementById('name-error'); const emailInput = document.getElementById('email'); const emailError = document.getElementById('email-error');
let isValid = true;
// Validar nome if (!nameInput.value.trim()) { nameError.textContent = 'Por favor, informe seu nome'; nameInput.setAttribute('aria-invalid', 'true'); isValid = false; } else { nameError.textContent = ''; nameInput.setAttribute('aria-invalid', 'false'); }
// Validar email if (!emailInput.value.trim() || !emailInput.value.includes('@')) { emailError.textContent = 'Por favor, informe um email válido'; emailInput.setAttribute('aria-invalid', 'true'); isValid = false; } else { emailError.textContent = ''; emailInput.setAttribute('aria-invalid', 'false'); }
if (isValid) { // Enviar formulário console.log('Formulário válido, enviando...'); }});Navegação por Teclado
Garanta que todos os elementos interativos sejam acessíveis via teclado:
/* Estilo de foco visível */:focus { outline: 3px solid #4d90fe; outline-offset: 2px;}
/* Estilo de foco apenas para navegação por teclado */:focus:not(:focus-visible) { outline: none;}
:focus-visible { outline: 3px solid #4d90fe; outline-offset: 2px;}Ordem de tabulação lógica:
<div class="card"> <!-- tabindex="0" permite que elementos não focáveis recebam foco --> <div class="card-header" tabindex="0">Título do Card</div>
<!-- tabindex="-1" remove o elemento da ordem de tabulação, mas ainda permite foco programático --> <div class="card-details" tabindex="-1">Detalhes do card</div>
<!-- Elementos naturalmente focáveis não precisam de tabindex --> <button class="card-action">Ação</button></div>ARIA (Accessible Rich Internet Applications)
Use atributos ARIA para melhorar a acessibilidade de componentes complexos:
<!-- Exemplo de menu dropdown acessível --><div class="dropdown"> <button id="dropdown-toggle" aria-haspopup="true" aria-expanded="false" aria-controls="dropdown-menu" > Menu </button>
<ul id="dropdown-menu" role="menu" aria-labelledby="dropdown-toggle" hidden > <li role="menuitem"><a href="/">Home</a></li> <li role="menuitem"><a href="/produtos">Produtos</a></li> <li role="menuitem"><a href="/contato">Contato</a></li> </ul></div>JavaScript para controlar o dropdown:
const toggleButton = document.getElementById('dropdown-toggle');const menu = document.getElementById('dropdown-menu');
toggleButton.addEventListener('click', () => { const expanded = toggleButton.getAttribute('aria-expanded') === 'true'; toggleButton.setAttribute('aria-expanded', !expanded);
if (expanded) { menu.hidden = true; } else { menu.hidden = false; // Foca o primeiro item do menu menu.querySelector('[role="menuitem"] a').focus(); }});
// Fechar o menu ao pressionar Escapemenu.addEventListener('keydown', (event) => { if (event.key === 'Escape') { toggleButton.setAttribute('aria-expanded', 'false'); menu.hidden = true; toggleButton.focus(); }});Regiões de Live
Use aria-live para anunciar atualizações dinâmicas:
<!-- Notificação de toast --><div class="toast" role="status" aria-live="polite" aria-atomic="true"> Operação concluída com sucesso!</div>
<!-- Área de carregamento --><div class="loading-area" aria-live="assertive" aria-busy="true"> Carregando dados, por favor aguarde...</div>JavaScript para atualizar a região live:
function showNotification(message) { const toast = document.querySelector('.toast');
// Limpa o conteúdo atual toast.textContent = '';
// Força o leitor de tela a anunciar a nova mensagem setTimeout(() => { toast.textContent = message; toast.classList.add('visible');
// Esconde após 5 segundos setTimeout(() => { toast.classList.remove('visible'); }, 5000); }, 50);}
// Usodocument.querySelector('.save-button').addEventListener('click', () => { // Salva os dados saveData() .then(() => { showNotification('Dados salvos com sucesso!'); }) .catch(error => { showNotification(`Erro ao salvar: ${error.message}`); });});Componentes Acessíveis em Astro
Componente de Modal Acessível
---const { id, title } = Astro.props;---
<div id={id} class="modal" role="dialog" aria-labelledby={`${id}-title`} aria-modal="true" hidden> <div class="modal-content"> <header class="modal-header"> <h2 id={`${id}-title`}>{title}</h2> <button type="button" class="close-button" aria-label="Fechar modal" > × </button> </header>
<div class="modal-body"> <slot /> </div>
<footer class="modal-footer"> <slot name="footer" /> </footer> </div></div>
<script> class AccessibleModal { constructor(modalElement) { this.modal = modalElement; this.openButton = document.querySelector(`[aria-controls="${this.modal.id}"]`); this.closeButton = this.modal.querySelector('.close-button'); this.focusableElements = this.modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); this.firstFocusable = this.focusableElements[0]; this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
this.previouslyFocused = null;
this.bindEvents(); }
bindEvents() { if (this.openButton) { this.openButton.addEventListener('click', () => this.open()); }
this.closeButton.addEventListener('click', () => this.close());
this.modal.addEventListener('keydown', (e) => this.handleKeyDown(e));
// Fechar ao clicar fora do modal this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); }
open() { this.previouslyFocused = document.activeElement;
this.modal.hidden = false;
// Foca o primeiro elemento focável if (this.firstFocusable) { this.firstFocusable.focus(); } else { this.closeButton.focus(); }
// Impede a rolagem do body document.body.style.overflow = 'hidden'; }
close() { this.modal.hidden = true;
// Restaura a rolagem do body document.body.style.overflow = '';
// Restaura o foco para o elemento que abriu o modal if (this.previouslyFocused) { this.previouslyFocused.focus(); } }
handleKeyDown(event) { if (event.key === 'Escape') { this.close(); return; }
// Trap focus dentro do modal if (event.key === 'Tab') { if (event.shiftKey && document.activeElement === this.firstFocusable) { event.preventDefault(); this.lastFocusable.focus(); } else if (!event.shiftKey && document.activeElement === this.lastFocusable) { event.preventDefault(); this.firstFocusable.focus(); } } } }
// Inicializa todos os modais na página function initModals() { const modals = document.querySelectorAll('.modal'); modals.forEach(modal => new AccessibleModal(modal)); }
// Inicializa quando o DOM estiver pronto document.addEventListener('DOMContentLoaded', initModals);
// Reinicializa após navegação com View Transitions document.addEventListener('astro:page-load', initModals);</script>
<style> .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal[hidden] { display: none; }
.modal-content { background-color: white; border-radius: 4px; max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid #eee; }
.modal-body { padding: 1rem; }
.modal-footer { padding: 1rem; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 0.5rem; }
.close-button { background: none; border: none; font-size: 1.5rem; cursor: pointer; padding: 0.25rem 0.5rem; border-radius: 4px; }
.close-button:hover, .close-button:focus { background-color: #f0f0f0; }</style>Uso do componente Modal:
---import Modal from '../components/Modal.astro';---
<button id="open-modal-button" aria-controls="example-modal" aria-haspopup="dialog"> Abrir Modal</button>
<Modal id="example-modal" title="Exemplo de Modal Acessível"> <p>Este é um exemplo de modal acessível construído com Astro.</p> <p>Ele pode ser navegado completamente por teclado e é compatível com leitores de tela.</p>
<div slot="footer"> <button id="cancel-button">Cancelar</button> <button id="confirm-button" class="primary">Confirmar</button> </div></Modal>
<script> // Script adicional para o botão de cancelar document.getElementById('cancel-button')?.addEventListener('click', () => { const modal = document.getElementById('example-modal'); if (modal) { // Dispara o evento de clique no botão de fechar modal.querySelector('.close-button')?.click(); } });</script>Ferramentas e Testes de Acessibilidade
Ferramentas de Desenvolvimento
Extensões de Navegador
- axe DevTools - Extensão para Chrome e Firefox
- WAVE Web Accessibility Evaluation Tool
- Lighthouse - Integrado ao Chrome DevTools
Ferramentas de Linha de Comando
# Instalar pa11ynpm install -g pa11y
# Executar teste de acessibilidadepa11y https://example.comTestes Automatizados de Acessibilidade
Integração com Playwright
import { test, expect } from '@playwright/test';import AxeBuilder from '@axe-core/playwright';
test.describe('Testes de Acessibilidade', () => { test('página inicial deve passar na verificação de acessibilidade', async ({ page }) => { await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]); });
test('formulário de contato deve ser acessível', async ({ page }) => { await page.goto('/contato');
// Verificar se o formulário tem labels associados const formInputs = await page.locator('form input, form select, form textarea'); const count = await formInputs.count();
for (let i = 0; i < count; i++) { const input = formInputs.nth(i); const id = await input.getAttribute('id');
if (id) { const label = await page.locator(`label[for="${id}"]`); expect(await label.count()).toBeGreaterThan(0); } }
// Verificar acessibilidade com axe const results = await new AxeBuilder({ page }) .include('form') .analyze();
expect(results.violations).toEqual([]); });});Lista de Verificação de Acessibilidade
Use esta lista para verificar a acessibilidade do seu site:
Conteúdo e Semântica
- Uso apropriado de elementos HTML semânticos
- Estrutura hierárquica de cabeçalhos (h1-h6)
- Texto alternativo para imagens
- Legendas e transcrições para vídeos e áudios
- Uso de listas para conteúdo em lista
Navegação e Interação
- Todos os elementos interativos são acessíveis por teclado
- Ordem de tabulação lógica
- Indicadores de foco visíveis
- Links com texto descritivo
- Skip links para pular para o conteúdo principal
Formulários
- Labels associados a campos de formulário
- Agrupamento de campos relacionados com fieldset e legend
- Mensagens de erro acessíveis
- Instruções claras para preenchimento
Design e Layout
- Contraste suficiente entre texto e fundo
- Site utilizável com zoom de 200%
- Layout responsivo
- Espaçamento adequado entre elementos clicáveis
- Texto redimensionável sem perda de funcionalidade
ARIA e JavaScript
- Uso correto de atributos ARIA
- Regiões live para conteúdo dinâmico
- Componentes complexos (tabs, modais, etc.) seguem padrões de acessibilidade
- Funcionalidade mantida com JavaScript desativado (quando possível)
Conclusão
A acessibilidade web não é apenas uma obrigação legal em muitos países, mas também uma prática ética que amplia o alcance do seu site. Ao seguir as diretrizes e implementar as práticas recomendadas neste documento, você estará criando soluções web que podem ser utilizadas por todas as pessoas, independentemente de suas habilidades ou deficiências.
Lembre-se de que a acessibilidade é um processo contínuo. Teste regularmente seu site com ferramentas automatizadas e, quando possível, com usuários reais que utilizam tecnologias assistivas.
Os próximos documentos detalharão outros aspectos do desenvolvimento:
- SEO e Performance: Otimização para mecanismos de busca e performance
- Implantação e DevOps: Estratégias de implantação e operações
Última atualização: 21 de março de 2025