Acessibilidade (a11y)
Diretrizes abrangentes de acessibilidade baseadas na WCAG 2.2 e auditorias de acessibilidade do Lighthouse. Objetivo: tornar o conteúdo utilizável por todos, incluindo pessoas com deficiência.
Fluxo de trabalho de auditoria baseado em evidências
Quando uma página renderizada estiver disponível:
- Execute uma auditoria de acessibilidade ao vivo do Lighthouse quando essa capacidade estiver disponível; com Chrome DevTools MCP, use
lighthouse_audit. Use o modo de navegação móvel para uma página pública geral ou o modo de snapshot quando recarregar perderia o estado autenticado ou criado pelo usuário. - Use os nós de auditoria com falha para localizar o componente ou modelo relevante em vez de pesquisar todo o repositório por padrões genéricos.
- Inspecione um snapshot da árvore de acessibilidade renderizada para nomes, funções, estados, landmarks e estrutura de títulos; com Chrome DevTools MCP, use
take_snapshot. Exercite o fluxo afetado com o teclado. - Corrija a origem e execute novamente a mesma auditoria e interação manual.
Se as ferramentas ao vivo não estiverem disponíveis, use o CLI do Lighthouse ou axe para cobertura automatizada e conclua as mesmas verificações manuais. Ferramentas automatizadas detectam apenas um subconjunto de barreiras de acessibilidade: uma pontuação de 100 não é conformidade com WCAG, e uma pontuação baixa não substitui a evidência no nível do problema.
Princípios WCAG: POUR
| Princípio | Descrição |
|---|---|
| Percebível | O conteúdo pode ser percebido por diferentes sentidos |
| Operável | A interface pode ser operada por todos os usuários |
| Understandable | O conteúdo e a interface são compreensíveis |
| Robusto | O conteúdo funciona com tecnologias assistivas |
Níveis de conformidade
| Nível | Requisito | Alvo |
|---|---|---|
| A | Acessibilidade mínima | Deve passar |
| AA | Conformidade padrão | Deve passar (requisito legal em muitas jurisdições) |
| AAA | Acessibilidade aprimorada | Bom ter |
Percebível
Alternativas textuais (1.1)
Imagens precisam de texto alternativo:
<!-- ❌ Alt ausente -->
<img src="chart.png">
<!-- ✅ Alt descritivo -->
<img src="chart.png" alt="Gráfico de barras mostrando aumento de 40% nas vendas do Q3">
<!-- ✅ Imagem decorativa (alt vazio) -->
<img src="decorative-border.png" alt="" role="presentation">
<!-- ✅ Imagem complexa com descrição mais longa -->
<figure>
<img src="infographic.png" alt="Infográfico de tendências de mercado 2024"
aria-describedby="infographic-desc">
<figcaption id="infographic-desc">
<!-- Descrição detalhada -->
</figcaption>
</figure>
Botões de ícone precisam de nomes acessíveis:
<!-- ❌ Sem nome acessível -->
<button><svg><!-- ícone do menu --></svg></button>
<!-- ✅ Usando aria-label -->
<button aria-label="Abrir menu">
<svg aria-hidden="true"><!-- ícone do menu --></svg>
</button>
<!-- ✅ Usando texto oculto visualmente -->
<button>
<svg aria-hidden="true"><!-- ícone do menu --></svg>
<span class="visually-hidden">Abrir menu</span>
</button>
Classe de ocultação visual:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Contraste de cor (1.4.3, 1.4.6)
| Tamanho do Texto | AA mínimo | AAA aprimorado |
|---|---|---|
| Texto normal (< 18px / < 14px negrito) | 4.5:1 | 7:1 |
| Texto grande (≥ 18px / ≥ 14px negrito) | 3:1 | 4.5:1 |
| Componentes de UI e gráficos | 3:1 | 3:1 |
/* ❌ Baixo contraste (2.5:1) */
.low-contrast {
color: #999;
background: #fff;
}
/* ✅ Contraste suficiente (7:1) */
.high-contrast {
color: #333;
background: #fff;
}
/* ✅ Estados de foco também precisam de contraste (3:1 contra o fundo, WCAG 1.4.11) */
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
Não confie apenas na cor:
<!-- ❌ Apenas a cor indica erro -->
<input class="error-border">
<style>.error-border { border-color: red; }</style>
<!-- ✅ Cor + ícone + texto -->
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error">
<span id="email-error" class="error-message">
<svg aria-hidden="true"><!-- ícone de erro --></svg>
Por favor, insira um endereço de e-mail válido
</span>
</div>
Alternativas de mídia (1.2)
<!-- Vídeo com legendas -->
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="Inglês" default>
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descrições">
</video>
<!-- Áudio com transcrição -->
<audio controls>
<source src="podcast.mp3" type="audio/mp3">
</audio>
<details>
<summary>Transcrição</summary>
<p>Texto completo da transcrição...</p>
</details>
Operável
Acessível por teclado (2.1)
Toda funcionalidade deve ser acessível por teclado. Prefira elementos interativos nativos — <button>, <a href>, e controles de formulário lidam com ativação por Enter/Espaço, foco e semântica para tecnologias assistivas gratuitamente. Adicione manipulação manual de teclado apenas quando não puder usar um elemento nativo.
<!-- ❌ Elemento não interativo com clique apenas: não focalizável, sem ativação por teclado -->
<div class="card" onclick="handleAction()">Abrir</div>
<!-- ✅ Melhor: use um botão nativo -->
<button type="button" onclick="handleAction()">Abrir</button>
// ✅ Quando você PRECISA usar um elemento não interativo (ex.: div com role="button"),
// torne-o focalizável E lide com a ativação por teclado. NÃO adicione isso a um <button>
// nativo — Enter/Espaço já disparam o clique, então você dispararia duas vezes.
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});
Sem armadilhas de teclado. Os usuários devem poder Tab para dentro e para fora de cada componente. Use o padrão de armadilha de foco modal para diálogos — o elemento nativo <dialog> lida com isso automaticamente.
Foco visível (2.4.7)
/* ❌ Nunca remova contornos de foco */
*:focus { outline: none; }
/* ✅ Use :focus-visible para foco apenas por teclado */
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid currentColor; /* herda a cor do texto → já verificado por contraste */
outline-offset: 2px;
}
/* ✅ Ou escolha uma cor da marca e verifique contraste ≥3:1 contra cada fundo onde aparecer */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
Foco não obscurecido (2.4.11) — novo na 2.2
Quando um elemento recebe foco por teclado, ele não deve estar completamente oculto por outro conteúdo criado pelo autor, como cabeçalhos fixos, rodapés ou painéis sobrepostos. No Nível AAA (2.4.12), nenhuma parte do elemento focalizado pode estar oculta.
/* ✅ Considere cabeçalhos fixos ao rolar para elementos focalizados */
:target {
scroll-margin-top: 80px;
}
/* ✅ Garanta que itens focalizados limpem barras fixas/pegajosas */
:focus {
scroll-margin-top: 80px;
scroll-margin-bottom: 60px;
}
Links de salto (2.4.1)
Forneça um link de salto para que usuários de teclado possam pular a navegação repetitiva. Veja o padrão de link de salto para marcação e estilos completos.
Tamanho do alvo (2.5.8) — novo na 2.2
Os alvos interativos devem ter pelo menos 24 × 24 pixels CSS (AA). Exceções: links de texto inline, elementos onde o navegador controla o tamanho e alvos onde um círculo de 24px centrado na caixa delimitadora não se sobrepõe a outro alvo.
/* ✅ Tamanho mínimo do alvo */
button,
[role="button"],
input[type="checkbox"] + label,
input[type="radio"] + label {
min-width: 24px;
min-height: 24px;
}
/* ✅ Tamanho confortável do alvo (recomendado 44×44) */
.touch-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
Movimentos de arrastar (2.5.7) — novo na 2.2
Qualquer ação que exija arrastar deve ter uma alternativa de ponteiro único (ex.: botões, inputs). Veja o padrão de movimentos de arrastar para um exemplo de lista classificável.
Temporização (2.2)
// Permita que os usuários estendam limites de tempo
function showSessionWarning() {
const modal = createModal({
title: 'Sessão Expirando',
content: 'Sua sessão expirará em 2 minutos.',
actions: [
{ label: 'Estender sessão', action: extendSession },
{ label: 'Sair', action: logout }
],
timeout: 120000
});
}
Movimento (2.3)
/* Respeite a preferência de movimento reduzido */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Compreensível
Idioma da página (3.1.1)
<!-- ❌ Nenhum idioma especificado -->
<html>
<!-- ✅ Idioma especificado -->
<html lang="pt">
<!-- ✅ Mudanças de idioma dentro da página -->
<p>A palavra francesa para olá é <span lang="fr">bonjour</span>.</p>
Navegação consistente (3.2.3)
<!-- A navegação deve ser consistente entre as páginas -->
<nav aria-label="Principal">
<ul>
<li><a href="/" aria-current="page">Início</a></li>
<li><a href="/produtos">Produtos</a></li>
<li><a href="/sobre">Sobre</a></li>
</ul>
</nav>
Ajuda consistente (3.2.6) — novo na 2.2
Se um mecanismo de ajuda (informações de contato, widget de chat, link de FAQ, opção de autoajuda) for repetido em várias páginas, ele deve aparecer na mesma ordem relativa sempre. Usuários que dependem de posicionamento consistente não devem ter que procurar ajuda em cada página.
Rótulos de formulário (3.3.2)
Cada input precisa de um rótulo programaticamente associado. Veja o padrão de rótulos de formulário para exemplos explícitos, implícitos e instrucionais.
Tratamento de erros (3.3.1, 3.3.3)
Anuncie erros para leitores de tela com role="alert" ou aria-live, defina aria-invalid="true" em campos inválidos e focalize o primeiro erro ao enviar. Veja o padrão de tratamento de erros para marcação completa e JS.
Entrada redundante (3.3.7) — novo na 2.2
Não force os usuários a reinserir informações que já forneceram na mesma sessão. Preencha automaticamente a partir de etapas anteriores ou permita que os usuários selecionem entre valores inseridos anteriormente. Exceções: reconfirmação de segurança e conteúdo que expirou.
<!-- ✅ Preenchimento automático do endereço de entrega a partir da cobrança -->
<fieldset>
<legend>Endereço de entrega</legend>
<label>
<input type="checkbox" id="same-as-billing" checked>
Igual ao endereço de cobrança
</label>
<!-- Campos preenchidos automaticamente quando marcado -->
</fieldset>
Autenticação acessível (3.3.8) — novo na 2.2
Os fluxos de login não devem depender de testes de função cognitiva (ex.: lembrar uma senha, resolver um quebra-cabeça) a menos que pelo menos um dos seguintes:
- Um mecanismo de copiar-colar ou preenchimento automático esteja disponível
- Um método alternativo exista (ex.: chave de acesso, SSO, link de e-mail)
- O teste use reconhecimento de objetos ou conteúdo pessoal (somente AA; AAA remove essa exceção)
<!-- ✅ Permita colar em campos de senha -->
<input type="password" id="password" autocomplete="current-password">
<!-- ✅ Ofereça alternativas sem senha -->
<button type="button">Entrar com chave de acesso</button>
<button type="button">Envie-me um link de login por e-mail</button>
Robusto
Uso de ARIA (4.1.2)
Prefira elementos nativos:
<!-- ❌ Função ARIA em div -->
<div role="button" tabindex="0">Clique em mim</div>
<!-- ✅ Botão nativo -->
<button>Clique em mim</button>
<!-- ❌ Checkbox ARIA -->
<div role="checkbox" aria-checked="false">Opção</div>
<!-- ✅ Checkbox nativo -->
<label><input type="checkbox"> Opção</label>
Quando ARIA for necessário, use as funções e estados corretos. Veja o padrão de abas ARIA para um exemplo completo de lista de abas.
Regiões ao vivo (4.1.3)
Use regiões aria-live para anunciar mudanças dinâmicas de conteúdo sem mover o foco. Veja o padrão de regiões ao vivo para marcação e um auxiliar showNotification().
Lista de verificação de teste
Teste automatizado
Prefira uma auditoria ao vivo do Lighthouse que retorne nós renderizados com falha diretamente para o agente. Com Chrome DevTools MCP, isso é lighthouse_audit. Caso contrário:
# Auditoria de acessibilidade do Lighthouse
npx lighthouse https://example.com --only-categories=accessibility
# axe-core
npm install @axe-core/cli -g
axe https://example.com
Teste manual
- Navegação por teclado: Tab através de toda a página, use Enter/Espaço para ativar
- Leitor de tela: Teste com VoiceOver (Mac), NVDA (Windows) ou TalkBack (Android)
- Zoom: Conteúdo utilizável com zoom de 200%
- Alto contraste: Teste com o Modo de Alto Contraste do Windows
- Movimento reduzido: Teste com
prefers-reduced-motion: reduce - Ordem de foco: Lógica e segue a ordem visual
- Tamanho do alvo: Elementos interativos atendem ao mínimo de 24×24px
Veja a referência de comandos do leitor de tela para atalhos do VoiceOver e NVDA.
Problemas comuns por impacto
Crítico (corrigir imediatamente)
- Rótulos de formulário ausentes
- Texto alternativo de imagem ausente
- Contraste de cor insuficiente
- Armadilhas de teclado
- Indicadores de foco ausentes
Grave (corrigir antes do lançamento)
- Idioma da página ausente
- Estrutura de títulos ausente
- Texto de link não descritivo
- Mídia com reprodução automática
- Links de salto ausentes
Moderado (corrigir em breve)
- Rótulos ARIA ausentes em ícones
- Navegação inconsistente
- Identificação de erro ausente
- Temporização sem controles
- Regiões de marco ausentes


