Estágio 02 · 02-02
LockedAcessibilidade não é "disclaimer pra evitar processo". É uma das métricas mais honestas da qualidade do seu frontend. Site acessível trabalha bem em screen reader, em teclado puro, em telas pequenas, em alto contraste, com prefers-reduced-motion, em conexões lentas. Site não-acessível geralmente também é bug-rico em outras dimensões.
Em mercados maduros (US, EU), a11y vira risco legal: ADA, EAA (European Accessibility Act, vigente desde 2025) tornam acessibilidade obrigatória pra muitas categorias de produto. Empresas que vendem nessas regiões já têm process de a11y review na pipeline.
Apesar disso, a maioria dos devs aprendeu a11y como "adiciona aria-label que tá bom". Esse hábito produz UI que parece OK na auditoria automática mas é inutilizável com NVDA ou VoiceOver. A diferença entre os dois é o que separa este módulo do tutorial qualquer.
Browsers expõem dois objetos paralelos pra cada página:
Tecnologias assistivas (screen readers como NVDA, JAWS, VoiceOver, TalkBack) leem da accessibility tree, não do DOM. Quando você escreve <div onClick>, o accessibility tree não vê um botão, vê um elemento genérico. Daí a mãe das melhores práticas: use elemento semântico nativo, ele já entra no a11y tree corretamente.
DevTools de qualquer browser moderno tem aba "Accessibility" que mostra a tree pro elemento selecionado. Use isso como código fonte verdadeiro.
WCAG 2.2 (atual, dezembro 2023) é o padrão mais aceito. Organiza em 4 princípios, POUR:
Cada critério tem 3 níveis: A (mínimo), AA (alvo realista, exigido pela maioria das leis), AAA (raro, ideal). Mire AA por default, exceções com justificativa.
Você não precisa decorar os 50+ critérios. Conhecer os 10-15 mais importantes (contraste 4.5:1, target size, focus visible, name/role/value, alt text, captions, page title, lang attribute, error identification, reflow) cobre 80% dos problemas reais.
Regra número 1 do ARIA: não use ARIA. Use o elemento HTML correto. ARIA é a ferramenta pra preencher buracos quando HTML não tem o conceito que você precisa.
ARIA tem três conceitos:
button, dialog, navigation, tab, tabpanel).aria-expanded="true", aria-checked="false", aria-busy, aria-current).aria-label, aria-labelledby, aria-describedby, aria-controls, aria-haspopup).Patterns oficiais estão em APG (ARIA Authoring Practices Guide), combobox, tabs, menu, tree, dialog modal, etc. Cada pattern tem o mínimo de roles + states + keyboard interactions.
Erros clássicos:
aria-label="Click here" em <button>, redundante, elemento já tem accessible name pelo conteúdo.role="button" em <a href>, quebra navegação. Use <button> ou ajuste.aria-hidden="true" em elemento com tabindex="0", usuário pode focar mas screen reader não anuncia. Loop confuso.aria-live mal usado: leitor anuncia em momento errado, ou não anuncia.Todo controle (button, link, input, etc.) precisa de accessible name: o que screen reader anuncia. A computação segue uma cadeia (accname spec):
aria-labelledby → texto dos elementos referenciados.aria-label → texto explícito.<label for="..."> (em inputs).title attribute (último recurso, ruim em mobile).Quando você compõe um botão com ícone só:
<button aria-label="Excluir pedido">
<svg aria-hidden="true">...</svg>
</button>
aria-hidden no SVG impede que o screen reader leia o conteúdo do SVG e duplique. aria-label no button dá o nome.
Todo controle interativo deve funcionar com teclado:
tabindex="0" adiciona ao tab order natural. tabindex="-1" remove do tab order mas mantém focusável programaticamente (pra mover foco via JS). Evite tabindex positivo (1, 2...), quebra ordem natural.
Focus management:
Focus trap em modal: Tab dentro do modal não escapa. Implementa-se interceptando keydown no boundary e redirecionando.
Focus visible: nunca remova outline sem substituir. :focus-visible (vs :focus) só mostra ring quando navegação é por teclado, não em click, resolve a guerra antiga.
WCAG AA exige:
Use oklch() ou ferramentas como WebAIM Contrast Checker, Stark plugin no Figma. Lighthouse e axe checam automaticamente.
Não use só cor pra transmitir informação. Estado de erro precisa ter ícone, mensagem, ou borda, não só "input vermelho". Daltonismo afeta ~8% dos homens.
Você não vai escrever a11y bom sem usar screen reader. Pelo menos uma vez. Recomendação:
Faça um exercício: navegue o seu site com olhos fechados e screen reader ligado. Os primeiros minutos são desorientadores; após 1h você nota tudo o que está errado em qualquer site.
UI moderna muda dinamicamente, toast notifications, validações inline, updates parciais. Screen reader não percebe automaticamente. Use live regions:
<div role="status" aria-live="polite">{message}</div>
aria-live="polite": anuncia quando idle. Use por default.aria-live="assertive": interrompe. Use só pra coisas críticas (erro de submit).role="status" ou role="alert": shorthand semântico.Cuidado: live region precisa existir no DOM antes do conteúdo ser inserido. Senão screen readers não detectam a mudança.
Forms são onde a11y mais aparece em apps reais. Boas práticas:
<label for="..."> ou <label> envolvendo o input.aria-invalid="true" no campo + aria-describedby apontando pra mensagem de erro.required HTML attribute (browser valida + screen reader anuncia).type="email", type="tel", type="url", inputmode="numeric", teclado mobile correto, validação básica.autocomplete="email", "new-password", "name", "shipping-postal-code", browser preenche, password manager funciona.role="dialog" + aria-modal="true" + aria-labelledby no título + focus trap + Esc fecha + foco volta.role="tablist" no container, role="tab" + aria-selected + aria-controls em cada tab, role="tabpanel" + aria-labelledby no painel. Arrow keys movem entre tabs.role="combobox", aria-expanded, aria-controls apontando pra role="listbox", aria-activedescendant pra opção destacada. Vale ler APG inteiro pra esse.role="status" ou role="alert". Não use role="alertdialog", esse é dialog que requer ação.<button role="switch" aria-checked="true"> ou input checkbox estilizado, ambos funcionam, semântica clara.<a href="#main">Skip to content</a>. Visível ao receber foco. Padrão obrigatório em sites com navegação extensa.Respeite preferências do usuário:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
prefers-color-scheme: dark pra dark mode automático. prefers-contrast: more pra modos de alto contraste.
Camadas:
eslint-plugin-jsx-a11y no React. Pega 30% dos erros sem rodar nada.axe-core (via @axe-core/playwright ou vitest-axe). Roda em CI. Pega ~50% dos issues automaticamente detectáveis.Lighthouse a11y score é util mas mente, score 100 não significa "site acessível", significa "passou checks automáticos". Combine com manual. 03-17 cobre testing automation profundamente.
WCAG 2.2 adicionou 9 success criteria sobre 2.1. Os mais impactantes pra eng frontend:
Implementations típicas:
outline: 2px solid + offset visível.autocomplete="current-password".ARIA APG (w3.org/WAI/ARIA/apg) define implementation oficial de cada widget complexo. Padrões essenciais a memorizar:
Modal Dialog:
<div role="dialog" aria-modal="true" aria-labelledby="title">
<h2 id="title">...</h2>
...
</div>
Combobox (autocomplete):
<input role="combobox" aria-expanded="false" aria-controls="listbox-1" aria-autocomplete="list" />
<ul id="listbox-1" role="listbox">
<li role="option" aria-selected="false">Opção</li>
</ul>
Disclosure (accordion):
<button aria-expanded="false" aria-controls="panel-1">Toggle</button>
<div id="panel-1" hidden>...</div>
Tabs:
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
</div>
<div id="panel-1" role="tabpanel">...</div>
Tree (árvore expansível): role="tree", "treeitem", aria-expanded, aria-level.
Grid (data grid): role="grid", "row", "gridcell". Arrow keys navegam células 2D. Enter ativa.
APG tem ~30 patterns. Bibliotecas (Radix, Headless UI, React Aria) implementam corretamente. Não reinvente.
Tooling pega ~30-40%. Manual cobre o resto. Checklist prática:
Heading hierarchy:
<h1> por página.Landmarks:
<header>, <nav>, <main>, <aside>, <footer>, exatamente um <main>.aria-label em landmarks duplicados ("breadcrumbs nav" + "primary nav").Tab order:
tabindex positivo (anti-pattern).Focus management:
Images / media:
alt="".Color:
Forms:
Reading order:
order em flex/grid quebra screen reader.Keyboard-only test: navegue site inteiro só com teclado. Falha = bug.
Screen reader test: VoiceOver (Cmd+F5 macOS) ou NVDA (Windows free). Tente fluxo crítico.
Brasil:
Português requer:
lang="pt-BR" correto pra screen reader pronunciation.LIBRAS (Língua Brasileira de Sinais) é língua oficial. Para conteúdo videos sérios, intérprete em vídeo opcional ou alternativa textual completa.
Você precisa, sem consultar:
aria-live="polite" vs "assertive" com casos.autocomplete e por que browser/password manager dependem deles.:focus de :focus-visible.Pegue o dashboard de logística do 02-01 e leve a a11y a AA real.
Auditoria inicial:
Refactor:
aria-hidden, funcional aria-label).Testes:
prefers-contrast).IntersectionObserver pra revelar conteúdo on-scroll precisa pensar em screen reader (announcing dynamic).Destrava
02-02 é prereq dos seguintes módulos: