| name | accessibility |
| description | Auditar e melhorar a acessibilidade web seguindo as diretrizes WCAG 2.1. Use quando solicitado para "melhorar acessibilidade", "auditoria a11y", "conformidade WCAG", "suporte a leitor de tela", "navegação por teclado" ou "tornar acessível". |
| license | MIT |
| metadata | {"author":"web-quality-skills","version":"1.0"} |
Acessibilidade (a11y)
Diretrizes abrangentes de acessibilidade baseadas em WCAG 2.1 e auditorias de acessibilidade do Lighthouse. Objetivo: tornar o conteúdo utilizável por todos, incluindo pessoas com deficiências.
Princípios WCAG: POUR
| Princípio | Descrição |
|---|
| Perceptível | O conteúdo pode ser percebido através de diferentes sentidos |
| Operável | A interface pode ser operada por todos os usuários |
| Compreensível | 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 (exigência legal em muitas jurisdições) |
| AAA | Acessibilidade aprimorada | Desejável |
Perceptível
Alternativas de texto (1.1)
Imagens exigem texto alternativo:
<img src="chart.png">
<img src="chart.png" alt="Gráfico de barras mostrando aumento de 40% nas vendas do Q3">
<img src="decorative-border.png" alt="" role="presentation">
<figure>
<img src="infographic.png" alt="Infográfico de tendências de mercado 2024"
aria-describedby="infographic-desc">
<figcaption id="infographic-desc">
</figcaption>
</figure>
Botões com ícones precisam de nomes acessíveis:
<button><svg></svg></button>
<button aria-label="Abrir menu">
<svg aria-hidden="true"></svg>
</button>
<button>
<svg aria-hidden="true"></svg>
<span class="visually-hidden">Abrir menu</span>
</button>
Classe de texto visualmente oculto:
.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 de 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 |
.low-contrast {
color: #999;
background: #fff;
}
.high-contrast {
color: #333;
background: #fff;
}
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Não dependa apenas de cor:
<input class="error-border">
<style>.error-border { border-color: red; }</style>
<div class="field-error">
<input aria-invalid="true" aria-describedby="email-error">
<span id="email-error" class="error-message">
<svg aria-hidden="true"></svg>
Por favor, insira um endereço de email válido
</span>
</div>
Alternativas para mídia (1.2)
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="pt" label="Português" default>
<track kind="descriptions" src="descriptions.vtt" srclang="pt" label="Descrições">
</video>
<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:
element.addEventListener('click', handleAction);
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});
Sem armadilhas de teclado:
function openModal(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
if (e.key === 'Escape') {
closeModal();
}
});
firstElement.focus();
}
Foco visível (2.4.7)
*:focus { outline: none; }
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}
Links para pular (2.4.1)
<body>
<a href="#main-content" class="skip-link">Pular para conteúdo principal</a>
<header></header>
<main id="main-content" tabindex="-1">
</main>
</body>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Timing (2.2)
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)
@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)
<html>
<html lang="pt-BR">
<p>A palavra em inglês para olá é <span lang="en">hello</span>.</p>
Navegação consistente (3.2.3)
<nav aria-label="Principal">
<ul>
<li><a href="/" aria-current="page">Início</a></li>
<li><a href="/products">Produtos</a></li>
<li><a href="/about">Sobre</a></li>
</ul>
</nav>
Rótulos de formulário (3.3.2)
<input type="email" placeholder="Email">
<label for="email">Endereço de email</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<label>
Endereço de email
<input type="email" name="email" autocomplete="email" required>
</label>
<label for="password">Senha</label>
<input type="password" id="password"
aria-describedby="password-requirements">
<p id="password-requirements">
Deve ter pelo menos 8 caracteres com um número.
</p>
Tratamento de erros (3.3.1, 3.3.3)
<form novalidate>
<div class="field" aria-live="polite">
<label for="email">Email</label>
<input type="email" id="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" class="error" role="alert">
Por favor, insira um endereço de email válido (ex: nome@exemplo.com)
</p>
</div>
</form>
form.addEventListener('submit', (e) => {
const firstError = form.querySelector('[aria-invalid="true"]');
if (firstError) {
e.preventDefault();
firstError.focus();
const errorSummary = document.getElementById('error-summary');
errorSummary.textContent = `${errors.length} erros encontrados. Por favor, corrija-os e tente novamente.`;
errorSummary.focus();
}
});
Robusto
HTML válido (4.1.1)
<div id="content">...</div>
<div id="content">...</div>
<a href="/"><button>Clique</button></a>
<div id="main-content">...</div>
<div id="sidebar-content">...</div>
<a href="/" class="button-link">Clique</a>
Uso de ARIA (4.1.2)
Prefira elementos nativos:
<div role="button" tabindex="0">Clique</div>
<button>Clique</button>
<div role="checkbox" aria-checked="false">Opção</div>
<label><input type="checkbox"> Opção</label>
Quando ARIA é necessária:
<div role="tablist" aria-label="Informações do produto">
<button role="tab" id="tab-1" aria-selected="true"
aria-controls="panel-1">Descrição</button>
<button role="tab" id="tab-2" aria-selected="false"
aria-controls="panel-2" tabindex="-1">Avaliações</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
</div>
Regiões dinâmicas (4.1.3)
<div aria-live="polite" aria-atomic="true" class="status">
</div>
<div role="alert" aria-live="assertive">
</div>
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`);
container.textContent = '';
requestAnimationFrame(() => {
container.textContent = message;
});
}
Checklist de testes
Testes automatizados
npx lighthouse https://example.com --only-categories=accessibility
npm install @axe-core/cli -g
axe https://example.com
Testes manuais
Comandos do leitor de tela
| Ação | VoiceOver (Mac) | NVDA (Windows) |
|---|
| Iniciar/Parar | ⌘ + F5 | Ctrl + Alt + N |
| Próximo item | VO + → | ↓ |
| Item anterior | VO + ← | ↑ |
| Ativar | VO + Space | Enter |
| Lista de headings | VO + U, depois setas | H / Shift + H |
| Lista de links | VO + U | K / Shift + K |
Problemas comuns por impacto
Críticos (corrigir imediatamente)
- Rótulos de formulário ausentes
- Texto alternativo de imagem ausente
- Contraste de cor insuficiente
- Armadilhas de teclado
- Sem indicadores de foco
Sérios (corrigir antes do lançamento)
- Idioma da página ausente
- Estrutura de heading ausente
- Texto de link não descritivo
- Mídia reproduzida automaticamente
- Links para pular ausentes
Moderados (corrigir em breve)
- Rótulos ARIA ausentes em ícones
- Navegação inconsistente
- Identificação de erro ausente
- Timing sem controles
- Regiões landmark ausentes
Referências