Pular para o conteúdo

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:

  1. Perceptível: As informações e os componentes da interface do usuário devem ser apresentados de maneira que possam ser percebidos.
  2. Operável: Os componentes de interface e a navegação devem ser operáveis.
  3. Compreensível: As informações e a operação da interface do usuário devem ser compreensíveis.
  4. 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...');
}
});

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 Escape
menu.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);
}
// Uso
document.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

src/components/Modal.astro
---
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"
>
&times;
</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

Ferramentas de Linha de Comando

Terminal window
# Instalar pa11y
npm install -g pa11y
# Executar teste de acessibilidade
pa11y https://example.com

Testes Automatizados de Acessibilidade

Integração com Playwright

tests/a11y/accessibility.spec.js
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
  • 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:


Última atualização: 21 de março de 2025