Especialista em Automação de Acessibilidade
Implemente e garanta a conformidade com WCAG 2.2 AA/AAA por meio de testes automatizados, navegação por teclado, padrões ARIA, otimização para leitores de tela e gerenciamento de foco.
Gatilhos de Ativação
Ativar em: falhas em auditorias de acessibilidade, requisitos de conformidade WCAG, navegação por teclado quebrada, leitor de tela não anunciando conteúdo, violações do axe-core, focus trap para modais/diálogos, dúvidas sobre atributos aria-*, links para pular navegação.
NÃO para: revisão de acessibilidade em nível de design (escolhas de cores, decisões de layout) -- use design-accessibility-auditor. Verificação pura de contraste de cores -- use css-in-js-architect com OKLCH.
Início Rápido
- Execute axe-core --
npx @axe-core/cli http://localhost:3000ou integre@axe-core/reactno modo de desenvolvimento para avisos no console. - Corrija primeiro as violações críticas -- texto alternativo ausente, rótulos de formulário ausentes, contraste insuficiente, landmarks ausentes.
- Implemente navegação por teclado -- todo elemento interativo deve ser alcançável e operável com Tab, Enter, Espaço, Escape e setas.
- Adicione gerenciamento de foco -- trave o foco em modais, restaure o foco ao fechar, gerencie o foco em mudanças de rota.
- Teste com um leitor de tela -- VoiceOver (macOS), NVDA (Windows) ou TalkBack (Android) para validação real.
Capacidades Principais
| Domínio | Tecnologias | Padrões Principais |
|---|---|---|
| Testes Automatizados | axe-core, Lighthouse, jest-axe, Playwright axe | Portões de acessibilidade em CI/CD |
| Navegação por Teclado | tabindex, onKeyDown, roving tabindex | Navegação por setas, grupos de foco |
| Leitores de Tela | Papéis ARIA, regiões live, aria-label | Anúncios, mudanças de estado, descrições |
| Gerenciamento de Foco | focus-visible, focus trap, atributo inert | Travamento de foco em modal, links para pular, foco em mudança de rota |
| HTML Semântico | <main>, <nav>, <article>, <aside> | Landmarks, hierarquia de cabeçalhos, listas |
| Formulários | <label>, aria-describedby, aria-invalid | Anúncio de erros, campos obrigatórios, fieldsets |
Padrões de Arquitetura
Padrão 1: axe-core automatizado no CI
// e2e/accessibility.spec.ts (Playwright + axe-core)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const pages = ['/', '/products', '/checkout', '/account'];
for (const path of pages) {
test(`${path} não tem violações de acessibilidade`, async ({ page }) => {
await page.goto(path);
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) // WCAG 2.2 AA
.analyze();
expect(results.violations).toEqual([]);
});
}
// Modo de desenvolvimento: axe-core no React (mostra violações no console)
// app/layout.tsx
if (process.env.NODE_ENV === 'development') {
import('@axe-core/react').then((axe) => {
axe.default(React, ReactDOM, 1000);
});
}
Padrão 2: Gerenciamento de Foco para Modais
import { useEffect, useRef, useCallback } from 'react';
function useFocusTrap(estaAberto: boolean) {
const containerRef = useRef<HTMLDivElement>(null);
const focusAnteriorRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!estaAberto) return;
// Salva o foco atual para restaurar depois
focusAnteriorRef.current = document.activeElement as HTMLElement;
// Foca no primeiro elemento focalizável
const container = containerRef.current;
if (!container) return;
const focalizaveis = container.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
focalizaveis[0]?.focus();
// Prende o foco dentro do container
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
const primeiro = focalizaveis[0];
const ultimo = focalizaveis[focalizaveis.length - 1];
if (e.shiftKey && document.activeElement === primeiro) {
e.preventDefault();
ultimo?.focus();
} else if (!e.shiftKey && document.activeElement === ultimo) {
e.preventDefault();
primeiro?.focus();
}
};
container.addEventListener('keydown', handleKeyDown);
// Usa inert no conteúdo de fundo
const conteudoPrincipal = document.querySelector('main');
conteudoPrincipal?.setAttribute('inert', '');
return () => {
container.removeEventListener('keydown', handleKeyDown);
conteudoPrincipal?.removeAttribute('inert');
focusAnteriorRef.current?.focus(); // restaura o foco
};
}, [estaAberto]);
return containerRef;
}
Padrão 3: Regiões ARIA Live para Conteúdo Dinâmico
// Anuncia erros de formulário, estados de carregamento e atualizações para leitores de tela
function useAnunciar() {
const anunciar = useCallback((mensagem: string, prioridade: 'polite' | 'assertive' = 'polite') => {
const el = document.getElementById(`aria-live-${prioridade}`);
if (el) {
el.textContent = ''; // Limpa primeiro para disparar o re-anúncio
requestAnimationFrame(() => { el.textContent = mensagem; });
}
}, []);
return anunciar;
}
// Monte uma vez no layout:
function RegioesAriaLive() {
return (
<>
<div id="aria-live-polite" aria-live="polite" aria-atomic="true" className="sr-only" />
<div id="aria-live-assertive" aria-live="assertive" aria-atomic="true" className="sr-only" />
</>
);
}
┌─ Pirâmide de Testes de Acessibilidade ─────────────┐
│ │
│ ▲ Testes Manuais com Leitor de Tela │
│ ╱ ╲ (VoiceOver, NVDA — trimestral) │
│ ╱───╲ │
│ ╱ ╲ Playwright + axe-core E2E │
│ ╱ E2E ╲ (cada página, portão de CI) │
│ ╱─────────╲ │
│ ╱ ╲ jest-axe Testes de Componente │
│ ╱ Componente ╲ (por componente interativo) │
│ ╱───────────────╲ │
│ ╱ ╲ ESLint jsx-a11y │
│╱ Lint Estático ╲ (em cada commit) │
│╲___________________╱ │
└─────────────────────────────────────────────────────┘
Anti-Padrões
divcomonClickem vez debutton-- divs não têm interação por teclado, nem papel, nem foco. Use elementos semânticos<button>ou<a>. Se precisar usar uma div, adicionerole="button",tabindex="0"eonKeyDownpara Enter/Espaço.aria-labelem tudo -- a rotulagem excessiva cria ruído para usuários de leitores de tela. Prefira rótulos de texto visíveis; usearia-labelapenas quando o texto visível for impossível.- Ocultar contornos de foco --
outline: nonesem substituto torna a navegação por teclado invisível. Use:focus-visiblepara indicadores de foco apenas por teclado que não aparecem ao clicar com o mouse. - Link para pular navegação ausente -- usuários de teclado precisam percorrer toda a navegação com Tab em cada página. Adicione
<a href="#main" class="sr-only focus:not-sr-only">Pular para o conteúdo principal</a>como o primeiro elemento focalizável. - Notificações toast sem regiões live -- leitores de tela não detectam toasts adicionados dinamicamente. Use regiões
aria-live="polite"para anunciá-los.
Checklist de Qualidade
- Portão de CI do axe-core passa com zero violações em todas as páginas
-
eslint-plugin-jsx-a11yhabilitado sem avisos - Todas as imagens têm texto
altdescritivo (oualt=""para imagens decorativas) - Todas as entradas de formulário têm elementos
<label>associados (não apenas texto do placeholder) - Hierarquia de cabeçalhos é sequencial (
h1>h2>h3, sem pular) - Landmarks da página presentes:
<main>,<nav>,<header>,<footer> - Link para pular navegação é o primeiro elemento focalizável
- Foco visível em todos os elementos interativos (estilos
:focus-visibledefinidos) - Modais prendem o foco e restauram o foco ao fechar
- Mudanças de conteúdo dinâmico são anunciadas via regiões
aria-live - Proporções de contraste de cores atendem WCAG AA (4.5:1 texto normal, 3:1 texto grande)
- Toda funcionalidade é operável apenas com teclado (Tab, Enter, Espaço, Escape, setas)