---
id: a11y-audit-checklist
domain: web-craft
agents: [ux-design-expert]
when: "ao auditar a acessibilidade de uma página/app"
---

# A11y Audit Checklist — como AUDITAR acessibilidade (WCAG 2.2)

> **Este pack é o avesso do APG.** O APG (ARIA Authoring Practices) ensina **como CONSTRUIR** um
> combobox, um menu, um dialog. Este pack ensina **como AUDITAR** — pegar uma página pronta e provar,
> categoria por categoria, se ela passa ou falha, com o critério WCAG e o teste exato. APG = receita;
> isto = inspeção.

## O problema

Acessibilidade "auditada" mal vira teatro: rodar o axe DevTools, ver "0 violations", declarar
acessível e seguir. Isso é falso. O **axe-core encontra ~30–57% dos problemas WCAG** (Deque mede ~57%
no melhor caso; estudos independentes ficam em 30–40%). Um scan limpo significa que você resolveu **a
parte automatizável** — sobram 40–70% que só aparecem com teclado, leitor de tela e olho humano.

Os tells de uma auditoria fake:
- **"Passou no axe" como veredito final.** O axe não sabe se o `alt` é significativo, se a ordem de
  leitura faz sentido, se o anúncio do leitor de tela é compreensível. Ele valida sintaxe, não semântica.
- **Nunca se navegou pela página só com Tab.** Foco invisível, ordem ilógica e armadilhas de teclado
  (keyboard trap) não aparecem em scan estático.
- **Nunca se ligou um leitor de tela.** Botão de ícone sem nome acessível, ordem de leitura quebrada,
  `aria-label` que mente — tudo passa batido.
- **Contraste "no olho".** Sem medir, ninguém vê que `#999` sobre branco dá 2.85:1 e **falha**.
- **Auditou o estado feliz.** Não testou foco visível, erro de formulário, estado `:invalid`, modal aberto.

A régua deste pack: **toda afirmação de conformidade aponta para um critério WCAG 2.2 numerado + um
teste reproduzível.** "Parece acessível" não é veredito.

## O conhecimento

### Os números WCAG 2.2 que você precisa de cor (fonte: w3.org)

| Critério | Nível | Número exato | Não confunda |
|---|---|---|---|
| **1.4.3** Contraste (Mínimo) | AA | Texto normal **4.5:1**; texto grande **3:1** | "Grande" = **≥18pt (~24px)** OU **≥14pt bold (~18.5px)**. `4.499:1` **falha** — não arredonde. |
| **1.4.11** Contraste de não-texto | AA | **3:1** | Bordas de input, ícones de UI, foco, estado checked, partes de gráfico que carregam significado. Hover NÃO precisa contrastar com o default. |
| **2.4.7** Foco visível | AA | Indicador de foco visível | Default do browser conta, mas se você matou o outline tem que repor. |
| **2.4.11** Foco não obscurecido (Mín.) | AA | Foco ao menos **parcialmente** visível | **F110**: header/footer sticky que cobre 100% o elemento focado = falha. Resolva com `scroll-padding`. |
| **2.4.13** Aparência do foco | AAA | Foco com área ≥ perímetro de 2px e **3:1** entre focado/não-focado | AAA — meta, não obrigação AA. |
| **2.5.7** Movimentos de arrastar | AA | Toda ação de drag precisa de **alternativa de clique único** | Slider, kanban, reorder por drag. |
| **2.5.8** Tamanho do alvo (Mín.) | AA | Alvo de ponteiro **≥24×24 CSS px** | Exceções: spacing (círculo de 24px não intersecta), equivalente na mesma página, inline no texto, controle do UA, essencial (pin de mapa). |
| **3.3.7** Entrada redundante | A | Não pedir de novo o que já foi informado | Auto-preencher ou deixar selecionar. |
| **3.3.8** Autenticação acessível (Mín.) | AA | Sem teste de função cognitiva (decorar, transcrever) sem alternativa | Deixe colar senha; permita gerenciador de senha. |

### Landmarks e ordem de headings — o esqueleto navegável

O leitor de tela navega por landmarks (`D` no NVDA) e por headings (`H`). Se o esqueleto está quebrado,
a página é inutilizável às cegas, mesmo com axe limpo.

```html
<!-- Estrutura de landmark correta: cada região de conteúdo tem seu elemento -->
<header>…</header>          <!-- role=banner implícito (1 por página)   -->
<nav aria-label="Principal">…</nav>
<main>…</main>              <!-- role=main implícito (EXATAMENTE 1)      -->
<aside aria-label="Relacionados">…</aside>
<footer>…</footer>          <!-- role=contentinfo implícito (1 por página) -->
```

Regras de auditoria de headings (SC 1.3.1):
- **Exatamente um `<h1>`** por página, descrevendo o conteúdo principal.
- **Hierarquia sem pulos**: `h1 → h2 → h3`. Nunca `h2 → h4`. Pular nível confunde a navegação.
- Heading é para **estrutura**, não tamanho. "Quero texto grande" não justifica um `<h2>`.
- Dois `<nav>` no DOM? Cada um precisa de `aria-label` distinto pra serem distinguíveis.

### Foco visível — o CSS que separa acessível de hostil

`outline: none` sem reposição é o pecado capital. O moderno:

```css
/* Foco SÓ por teclado — não polui o clique de mouse, mas garante teclado */
:focus-visible {
  outline: 2px solid var(--focus);     /* 2px é o mínimo de 2.4.13   */
  outline-offset: 2px;                  /* respira; não some na borda */
}
/* Nunca remova o foco sem repor algo perceptível */
:focus:not(:focus-visible) { outline: none; }   /* ok: tira só do mouse */

/* Sticky header NÃO pode comer o foco (2.4.11 / F110) */
html { scroll-padding-top: 5rem; }       /* altura do header sticky    */

/* O indicador de foco precisa de 3:1 contra o fundo (1.4.11) */
```

Teste: o contraste do anel de foco contra o fundo adjacente tem que bater **3:1**. Um foco azul claro
sobre fundo claro falha.

### Formulários — o trio label / describedby / invalid

A maioria das falhas de form é o leitor de tela não saber o que é o campo, qual a dica, ou que deu erro.

```html
<label for="email">E-mail</label>
<input
  id="email"
  type="email"
  autocomplete="email"
  aria-describedby="email-hint email-err"
  aria-invalid="true"
/>
<p id="email-hint">Usaremos só para o recibo.</p>
<p id="email-err" role="alert">E-mail inválido — falta o @.</p>
```

Régua de auditoria:
- **Todo input tem `<label>` associado** (`for`/`id`), não placeholder fazendo de label. Placeholder
  some ao digitar e costuma falhar contraste.
- **Dica e erro ligados via `aria-describedby`** — o leitor anuncia junto com o campo.
- **`aria-invalid="true"`** só no campo em erro, e a mensagem é texto (não só borda vermelha — cor
  sozinha viola 1.4.1).
- **Grupos** (radios, checkboxes relacionados) em `<fieldset>` + `<legend>`.
- **`autocomplete`** correto (`email`, `name`, `tel`) — atende 1.3.5 e 3.3.7 (entrada redundante).
- Erros resumidos **em lista no topo do form**, cada item linkando ao campo.

### Alt text e SVG acessível — a árvore de decisão

`alt` não é "descreva a imagem", é "qual o **propósito** dela aqui". A mesma foto tem alt diferente em
contextos diferentes.

| Imagem | Decisão |
|---|---|
| Informativa (gráfico, foto que carrega conteúdo) | `alt` descreve a informação, não a aparência |
| Decorativa (textura, divisor, blob) | `alt=""` (vazio, presente) — remove da árvore de acessibilidade |
| Dentro de link/botão sem texto | O alt/label carrega o **destino**: `alt="Página inicial"`, não `alt="logo"` |
| Complexa (gráfico de dados) | Alt curto + descrição longa adjacente (texto ou `<figcaption>`) |

SVG (fonte: CSS-Tricks, MDN) — três padrões, escolha um:

```html
<!-- 1. SVG DECORATIVO (ícone ao lado de texto que já diz tudo) -->
<svg aria-hidden="true" focusable="false">…</svg>

<!-- 2. SVG SIGNIFICATIVO standalone (carrega sentido sozinho) -->
<svg role="img" aria-label="Aviso de erro">…</svg>

<!-- 3. BOTÃO/LINK SÓ DE ÍCONE: nome no controle, ícone escondido -->
<button aria-label="Fechar">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>
```

Tell de auditoria: botão de ícone sem texto visível **e** sem `aria-label` → o leitor anuncia "botão"
sem nome. Falha 4.1.2. `focusable="false"` em SVG dentro de link é cinto-e-suspensório (legado do IE),
mas mantém previsível.

### Reduced motion — respeite quem pede menos

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
```

Auditoria: ative "Reduzir movimento" no SO e recarregue. Parallax, auto-carousel, fade-up em scroll —
tudo deve cessar ou virar instantâneo. Animação que pisca >3×/s é gatilho de convulsão (2.3.1) e é
**bloqueante**, não negociável.

### O teste que o axe NÃO faz — o protocolo manual

Auditoria de verdade tem 3 camadas. Pular qualquer uma é auditoria fake.

1. **axe-core** (DevTools ou `@axe-core/playwright`): pega o automatizável (~30–57%). É o piso, não o teto.
2. **Só teclado**: desconecte o mouse. `Tab` por toda a página.
   - Chega em todo controle? A ordem segue o visual? O foco é sempre visível? Dá pra **sair** de
     todo widget (sem keyboard trap)? `Esc` fecha modal? `Enter`/`Espaço` acionam?
3. **Leitor de tela** (NVDA no Windows, VoiceOver no Mac — `Cmd+F5`): 10 minutos pegam o que nenhum
   scanner pega.
   - Navegue por headings (`H`) e landmarks. Faz sentido a estrutura?
   - Todo botão/link tem **nome** anunciado? Imagens informativas têm alt útil? O erro de form é
     **anunciado** ao ocorrer (`role="alert"`)? A ordem de leitura bate com a visual?

## Checklist

Audite por categoria. Qualquer item marcado é uma **falha com critério WCAG** a corrigir.

**Estrutura (landmarks + headings)**
- [ ] Existe exatamente **um `<main>`** e exatamente **um `<h1>`**?
- [ ] Headings sem pulo de nível (sem `h2 → h4`)? (1.3.1)
- [ ] `<header>/<nav>/<main>/<footer>` usados como landmarks; `<nav>` repetidos têm `aria-label` distinto?
- [ ] `<html lang="pt-BR">` presente? `<title>` único e descritivo por página?

**Foco e teclado**
- [ ] Todo controle é alcançável por `Tab` e o **foco é sempre visível** (2.4.7)?
- [ ] Ordem de tabulação segue o layout visual; sem `tabindex` positivo?
- [ ] Nenhuma **armadilha de teclado**; `Esc` fecha modais/overlays?
- [ ] Header/footer sticky **não cobre 100%** o elemento focado (2.4.11 / F110)?
- [ ] Anel de foco tem **≥3:1** contra o fundo (1.4.11)?

**Formulários**
- [ ] Todo input tem `<label>` associado (não só placeholder)?
- [ ] Dica e erro ligados via `aria-describedby`; erro também é **texto**, não só cor (1.4.1)?
- [ ] Campo em erro tem `aria-invalid="true"` e a mensagem usa `role="alert"`?
- [ ] Grupos em `<fieldset>`/`<legend>`; `autocomplete` correto (3.3.7)?

**Imagens e SVG**
- [ ] Toda `<img>` informativa tem `alt` que descreve o **propósito**?
- [ ] Imagens decorativas têm `alt=""` (presente e vazio)?
- [ ] SVG decorativo: `aria-hidden="true"`; SVG significativo: `role="img"` + `aria-label`?
- [ ] Botão/link só de ícone tem `aria-label` no **controle** (4.1.2)?

**Contraste (medido, não no olho)**
- [ ] Texto normal **≥4.5:1**; texto grande (≥18pt / ≥14pt bold) **≥3:1** (1.4.3)?
- [ ] Bordas de input, ícones de UI e indicador de foco **≥3:1** (1.4.11)?
- [ ] Texto sobre imagem/gradiente medido no ponto de pior contraste?
- [ ] `::selection` customizado ainda tem contraste suficiente?
- [ ] Nenhuma informação transmitida **só por cor** (1.4.1)?

**Movimento e toque**
- [ ] `prefers-reduced-motion: reduce` desliga/instantaneíza animações?
- [ ] Nenhum flash >3×/s (2.3.1, bloqueante)?
- [ ] Alvos de ponteiro **≥24×24 CSS px** ou com spacing equivalente (2.5.8)?
- [ ] Ação de drag tem alternativa de clique único (2.5.7)?

**Protocolo de teste (as 3 camadas)**
- [ ] axe-core rodado e zerado (piso, ~30–57%)?
- [ ] Página percorrida **só com teclado**?
- [ ] Página ouvida em **leitor de tela** (NVDA/VoiceOver), estrutura e nomes conferidos?

## Tabela de decisão

| Sintoma observado | Critério WCAG | Veredito | Correção |
|---|---|---|---|
| `outline: none` sem reposição | 2.4.7 AA | **FAIL** | `:focus-visible { outline: 2px solid }` |
| Texto cinza `#999` sobre branco (2.85:1) | 1.4.3 AA | **FAIL** | Escurecer até **≥4.5:1** |
| Borda de input quase invisível (1.8:1) | 1.4.11 AA | **FAIL** | Borda **≥3:1** contra o fundo |
| Header sticky cobre 100% o item focado | 2.4.11 AA | **FAIL** | `scroll-padding-top` = altura do header |
| Botão de ícone sem texto nem `aria-label` | 4.1.2 A | **FAIL** | `aria-label` no botão; SVG `aria-hidden` |
| Placeholder no lugar de `<label>` | 1.3.1 / 4.1.2 | **FAIL** | `<label for>` real associado |
| Erro de form só em vermelho, sem texto | 1.4.1 AA | **FAIL** | Mensagem em texto + `role="alert"` |
| Pulo de heading `h2 → h4` | 1.3.1 A | **FAIL** | Hierarquia sequencial sem pulo |
| Dois `<main>` na página | 1.3.1 A | **FAIL** | Apenas um `<main>` |
| Alvo de toque 18×18 px, sem spacing | 2.5.8 AA | **FAIL** | Aumentar para **≥24×24** ou afastar |
| Carrossel auto-play ignora reduced-motion | 2.3.3 AAA / boa prática | **CONCERNS** | Honrar `prefers-reduced-motion` |
| Animação pisca 4×/s | 2.3.1 A | **FAIL (bloqueante)** | Remover/reduzir para <3×/s |
| axe limpo, mas nunca testado por teclado/leitor | — | **CONCERNS** | Rodar as 3 camadas antes de aprovar |
| Drag-and-drop sem alternativa de clique | 2.5.7 AA | **FAIL** | Adicionar ação de clique equivalente |
| Imagem decorativa com `alt="imagem decorativa"` | 1.1.1 A | **CONCERNS** | Trocar por `alt=""` |

---

**Fontes:** [WCAG 2.2 (W3C/TR)](https://www.w3.org/TR/WCAG22/) · [What's New in WCAG 2.2](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/) · [SC 1.4.3 Contrast Minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) · [SC 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html) · [SC 2.4.11 Focus Not Obscured](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum) · [F110](https://www.w3.org/WAI/WCAG22/Techniques/failures/F110.html) · [SC 2.5.8 Target Size](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) · [The A11Y Project Checklist](https://www.a11yproject.com/checklist/) · [Accessible SVG Icons — CSS-Tricks](https://css-tricks.com/accessible-svg-icons/) · [axe-core (Deque)](https://github.com/dequelabs/axe-core)
