Acessibilidade de frontend
Acessibilidade é parte de toda alteração na interface — projetada, construída e testada com ela. Uma tarefa de interface não está concluída até que funcione apenas com teclado, cada controle anuncie nome, função e estado, as verificações automatizadas passem e os testes consultem por função.
Alvo: WCAG 2.2 nível AA — na UE, a Lei Europeia de Acessibilidade (leis nacionais
como BFSG, EN 301 549) a torna obrigatória para muitos produtos. Uma constatação cita um critério WCAG A/AA ou uma tarefa concreta que
um usuário de tecnologia assistiva não consegue concluir. Critérios e convenções AAA (um <h1>,
alvos de 44 px) são recomendações, nunca bloqueadores.
| Tarefa | Leia |
|---|---|
| Escolher elementos, marcos (landmarks), cabeçalhos, ARIA, nomes acessíveis, estados desabilitados | references/semantics-and-aria.md |
| Anéis de foco, ordem de tabulação, link de skip (pular), diálogos, mudanças de rota, mapas de tecla de widget | references/focus-and-keyboard.md |
| Rótulos, preenchimento automático, tipos de entrada, erros de validação, comportamento de envio | references/forms.md |
| Anunciar mudanças, regiões ativas, toasts, texto alternativo, SVG, mídia | references/screen-readers.md |
| Contraste, cor, tamanhos de alvo, movimento, zoom, reflow, tratamento de conteúdo | references/visual-and-motion.md |
| Testes automatizados e manuais, CI | references/testing.md |
| Auditoria/revisão de interface existente, formato de relatório | references/audit.md |
| Consultar um critério WCAG 2.2 | references/wcag.md |
Regras principais — sempre
Nativo primeiro. <button> para ações, <a href> / <Link> do roteador para navegação,
<label>, <fieldset>, <table>, <dialog>, marcos (landmarks). Nunca <div onClick>. Nenhum ARIA
é melhor que ARIA ruim; uma função promete o modelo completo de teclado. Widgets complexos
(combobox, menu, seletor de data) vêm da biblioteca headless acessível do projeto, não
construídos manualmente.
Todo elemento interativo tem um nome acessível [4.1.2]. Botões com apenas ícone recebem
aria-label, o ícone aria-hidden="true". O texto do rótulo visível faz parte do
nome acessível [2.5.3]. Links dizem para onde vão — sem "clique aqui" [2.4.4].
Teclado [2.1.1, 2.1.2]. Tudo que funciona com um ponteiro funciona com o teclado;
nenhuma armadilha (trap). Ordem de tabulação = ordem visual; nunca um tabIndex positivo. Escape fecha a
camada sobreposta mais alta. Arrasto e gestos têm uma alternativa de ponteiro único [2.5.7, 2.5.1].
Foco visível [2.4.7, 1.4.11]. Estilize :focus-visible; nunca outline: none sem uma
substituição verificada (≥ 3:1 contra qualquer cor adjacente). O foco não é escondido atrás
de cabeçalhos/banners fixos [2.4.11] — scroll-padding-top.
Gerenciamento de foco. Diálogos: o foco entra ao abrir, fica preso (trapped), plano de fundo inerte,
retorna ao gatilho ao fechar. Mudança de rota em SPA: atualize document.title [2.4.2] e
mova o foco para o novo <h1> (tabIndex={-1}) ou <main>.
Formulários [1.3.1, 3.3.1, 3.3.2]. <label> visível para cada controle — um placeholder não é
um rótulo. type, inputMode, autoComplete corretos [1.3.5]. Erros inline, vinculados com
aria-describedby, campo aria-invalid="true", foque o primeiro campo inválido ao enviar.
Valide ao enviar — não desabilite o envio até que esteja válido. Nunca bloqueie colar (paste) [3.3.8].
Anuncie mudanças dinâmicas [4.1.3]. Específico do campo → aria-describedby; não urgente
(salvo, contagem de resultados, carregando) → role="status"; erro urgente no nível do formulário →
role="alert". A região ativa (live region) existe no DOM antes que seu texto mude.
Cor e contraste [1.4.3, 1.4.11, 1.4.1]. Texto ≥ 4.5:1 (grande ≥ 3:1), limites da interface, ícones e anéis de foco ≥ 3:1 — aplicado através de tokens de design. Cor nunca é o único indicador.
Alvos (Targets) [2.5.8]. ≥ 24×24 px CSS ou espaçamento suficiente; mire em 40 px desktop / 44 px
toque. Sobreposições decorativas recebem pointer-events: none.
Movimento e tempo [2.3.3, 2.2.2]. Animações opt-in via
@media (prefers-reduced-motion: no-preference); parallax/reprodução automática desligados em movimento reduzido.
Qualquer coisa em movimento > 5 s pode ser pausada. Toasts com ações ou erros permanecem até
serem dispensados.
Zoom e reflow [1.4.4, 1.4.10]. Funciona com 200 % do tamanho do texto e 320 px de largura CSS sem
rolagem horizontal. Nunca maximum-scale=1 / user-scalable=no.
Noções básicas da página. <html lang> definido e atualizado com o idioma da interface [3.1.1]; um
<main> visível; cabeçalhos formam um esboço; link de skip (pular) primeiro quando a navegação precede <main>
[2.4.1].
Imagens. Significativas → alt descreve o propósito; decorativas → alt=""; funcionais
(a imagem é o link/botão) → a ação [1.1.1].
Dever de teste
Toda alteração na interface acompanha:
- testes de componente que consultam por função e nome (
getByRole) e cobrem caminhos de teclado (user.tab(),{Escape},toHaveFocus()), - histórias do Storybook cuja verificação do addon-a11y é executada como erro (falha no CI),
- varreduras do Playwright +
@axe-core/playwright(tags WCAG 2.x A/AA) em páginas críticas e estados, - uma passagem manual apenas com teclado do fluxo; uma passagem com leitor de tela (VoiceOver/NVDA) para fluxos críticos.
Ferramentas automatizadas encontram apenas parte dos problemas — uma execução limpa do axe não é conformidade com
WCAG. Detalhes: references/testing.md.
Antes de finalizar — verifique a alteração para
| Padrão | Correção |
|---|---|
<div onClick / <span onClick | <button>, ou <a href> quando navega |
role="button"/role="tab" sem tratamento de tecla | elemento nativo ou o mapa de teclas do APG |
outline: none / outline-none sem substituição focus-visible | manter o anel do navegador ou anel personalizado verificado |
tabIndex positivo | corrigir ordem do DOM, usar 0 / -1 |
<img sem alt | alt="" ou uma descrição de propósito |
| botão de ícone sem nome | aria-label, ícone aria-hidden |
entrada sem <label> / placeholder como rótulo | rótulo visível vinculado |
disabled={!isValid} no envio | manter habilitado, validar ao enviar |
{msg && <div role="status">…} | renderizar a região vazia primeiro, atualizar seu texto |
aria-live="assertive" em mensagens de sucesso | role="status" |
aria-hidden em ou acima de um elemento focalizável | removê-lo ou tornar a subárvore inert |
onPaste + preventDefault | permitir colar |
maximum-scale=1 / user-scalable=no | remover |
cabeçalho fixo sem scroll-padding-top | preencher o scroller |
| arrasto/gesto sem alternativa de botão | adicionar um caminho de ponteiro único |
transition: all, movimento sem guarda de movimento reduzido | listar propriedades, envolver em no-preference |
Relatórios
Auditorias e revisões relatam no idioma da conversa, uma constatação por causa raiz com todos os locais,
critério WCAG, impacto no usuário e correção concreta — formato em references/audit.md.
A severidade mapeia para a habilidade de revisão de código: ALTA → Bloqueador/Major, MÉDIA → Major/Minor,
BAIXA → Minor.
Fontes
Combinado de: Addy Osmani's web-quality-skills accessibility (MIT), Jakub Krehel's
better-accessibility (MIT), ibelick's fixing-accessibility (MIT), Vercel's Web
Interface Guidelines (MIT); W3C WCAG 2.2 e WAI-ARIA Authoring Practices.