Acessibilidade é um daqueles tópicos onde a lacuna entre o que os times dizem e o que entregam é constrangedoramente grande. Já estive dos dois lados dessa lacuna. No início da minha carreira, entreguei interfaces que falhavam em verificações básicas de contraste. Com o tempo, liderando o redesign no Strongest Families Institute e depois entregando features em conformidade com acessibilidade no BMO, fiz da acessibilidade uma parte inegociável do meu próprio trabalho, nos designs que entrego no handoff e no código que escrevo. Este post é o mínimo destilado que cobro de mim mesmo e defendo em todo time em que entro.
Por Que WCAG 2.2 AA É Sua Linha de Base, Não Seu Teto
WCAG significa Web Content Accessibility Guidelines, e a linha de base a mirar é o nível AA da versão 2.2. Ela virou Recomendação do W3C em outubro de 2023 e substitui a 2.1, que agora é o alvo legado. O nível A cobre as falhas mais críticas. O nível AAA é aspiracional e frequentemente impraticável para aplicações complexas. AA fica no meio: alcançável com disciplina, significativo para usuários reais, e legalmente relevante no Canadá. Organizações reguladas em nível federal, como bancos, caem sob o Accessible Canada Act e o Canadian Human Rights Act. Ontário acrescenta a AODA, cujo regulamento de acessibilidade ainda aponta para o WCAG 2.0 AA, e outras províncias têm suas próprias leis. Um empregador regulado em nível federal que opera em Ontário pode estar sob mais de uma delas ao mesmo tempo. Construir para 2.2 AA atende a todas.
Se o seu projeto foi escopado sob a 2.1, a lacuna é pequena e vale a pena fechar. O WCAG 2.2 adiciona quatro critérios AA: 2.4.11 Focus Not Obscured (Minimum), 2.5.7 Dragging Movements, 2.5.8 Target Size (Minimum) em 24x24 pixels CSS, e 3.3.8 Accessible Authentication (Minimum). Ele também remove o 4.1.1 Parsing, que não se aplica mais. Faça uma revisão de lacunas contra esses cinco itens e você está em dia.
Em banco, conformidade AA não é opcional. O Accessible Canada Act coloca as organizações reguladas em nível federal sob o dever de identificar, remover e prevenir barreiras, então acessibilidade é tratada como uma categoria de risco ao lado de segurança e privacidade de dados, e as features que construí no BMO precisavam estar em conformidade com acessibilidade. Esse enquadramento é útil em qualquer lugar: quando você para de vender acessibilidade como um bem moral e começa a vender como mitigação de risco, a conversa com o negócio fica muito mais curta.
Contraste de Cores: A Falha Mais Comum
O WCAG AA exige uma taxa de contraste de pelo menos 4.5:1 para texto normal e 3:1 para texto grande (18pt regular ou 14pt bold). Essas taxas parecem técnicas mas a regra é simples: se seu texto corpo cinza sobre branco parece sutil e elegante, provavelmente falha.
Duas ferramentas que vale ter: o WebAIM Contrast Checker para qualquer coisa em que você consiga colar um par de hex, e um plugin de verificação de contraste dentro do Figma para que a checagem aconteça antes do handoff. No Figma, rodo verificações de contraste antes de qualquer design ir para dev. No Strongest Families, estabeleci o design system para o redesign da plataforma IRIS, e contraste foi uma das primeiras restrições embutidas nele: cada par de cores foi verificado contra o AA antes de entrar na paleta, o que também manteve a paleta pequena. Um benefício colateral que melhorou a consistência em todo o produto.
- Texto normal (abaixo de 18pt regular / 14pt bold): taxa mínima de 4.5:1
- Texto grande: taxa mínima de 3:1
- Componentes de UI e objetos gráficos: mínimo de 3:1 contra cores adjacentes
- Elementos decorativos sem valor informacional: sem requisito
- Texto placeholder em inputs: também deve atender 4.5:1. Frequentemente esquecido
- Elementos em estado desabilitado: explicitamente excluídos dos requisitos de contraste, mas use com moderação
Navegação por Teclado: Todo Elemento Interativo
Todo elemento interativo na sua UI deve ser alcançável e operável usando apenas um teclado. Isso significa links, botões, campos de formulário, modais, dropdowns, date pickers, componentes select customizados. Tudo. A ordem de tab deve ser lógica e corresponder à ordem visual de leitura.
O modo de falha que vejo com mais frequência em aplicações Angular são componentes customizados que engolem eventos de teclado. Um desenvolvedor constrói um dropdown bonito usando divs e click handlers. Funciona perfeitamente com mouse. Com teclado é completamente invisível. Trato navegação por teclado como parte dos critérios de aceitação de todo componente interativo que construo, esteja isso escrito no ticket ou não. Se não está no AC, não é testado, e se não é testado, não é entregue corretamente.
// Bad: div-based button with no keyboard support
// <div class="btn" (click)="submit()">Submit</div>
// Good: native button or explicit keyboard handler
// <button type="button" (click)="submit()">Submit</button>
// If you must use a non-semantic element:
@Component({
selector: 'app-custom-button',
template: `
<div
class="custom-btn"
role="button"
[attr.tabindex]="disabled ? -1 : 0"
[attr.aria-disabled]="disabled"
(click)="handleAction()"
(keydown.enter)="handleAction()"
(keydown.space)="handleAction()">
<ng-content></ng-content>
</div>
`
})
export class CustomButtonComponent {
@Input() disabled = false;
handleAction(): void {
if (!this.disabled) {
// emit action
}
}
}Minha preferência forte é sempre usar elementos HTML nativos. Button, a, input, select. Eles vêm com suporte a teclado, gerenciamento de foco e semântica ARIA embutidos. Todo componente customizado que você constrói para substituir um nativo é uma dívida que vai ter que pagar em atributos ARIA e event handlers.
Gerenciamento de Foco: A Parte Que Todo Mundo Esquece
Navegação por teclado é sobre alcançar elementos. Gerenciamento de foco é sobre para onde o foco vai após uma ação. Essa distinção importa enormemente em aplicações single-page.
Considere um diálogo modal. Um usuário pressiona Enter em um botão de acionamento. O modal abre. Onde está o foco? Se você não gerencia, o foco fica no botão de acionamento atrás do modal. O usuário usa tab para frente e cai em elementos no fundo que agora estão visualmente escondidos. Isso é uma falha do critério de sucesso 2.4.3 Focus Order, que exige que a ordem de foco preserve significado e operabilidade. E também é uma experiência genuinamente terrível para usuários de leitores de tela.
- Quando um modal abre: mova o foco para o container do modal ou seu primeiro elemento interativo
- Quando um modal fecha: retorne o foco para o elemento que o acionou
- Ao navegar entre rotas: mova o foco para o título do conteúdo principal ou o container da página
- Ao mostrar erros inline após envio de formulário: mova o foco para o primeiro erro ou para um resumo
- Ao expandir um accordion: o foco pode ficar no gatilho. Não mova a menos que o conteúdo seja carregado dinamicamente
- Prenda o foco dentro de modais: o usuário não deve conseguir sair com tab de um modal aberto para o conteúdo de fundo
Antes de escrever tudo isso por conta própria, olhe o @angular/cdk/a11y. Ele traz cdkTrapFocus, FocusTrapFactory, FocusMonitor e LiveAnnouncer, e em um projeto Angular você já tem o CDK em disco. Escreva um focus trap na mão só quando precisar de um comportamento que a diretiva não cobre. De qualquer forma, repare na guarda de lista vazia abaixo. Se o container não tem nada focável, first e last são undefined e chamar focus() neles lança erro.
// Focus trap service for modals in Angular
import { Injectable, ElementRef } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class FocusTrapService {
private focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
getFocusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll<HTMLElement>(this.focusableSelectors)
).filter(el => !el.closest('[hidden]'));
}
trapFocus(container: HTMLElement, event: KeyboardEvent): void {
if (event.key !== 'Tab') {
return;
}
const focusable = this.getFocusableElements(container);
if (focusable.length === 0) {
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}Labels ARIA: Use Corretamente ou Não Use
ARIA. Accessible Rich Internet Applications. É um conjunto de atributos HTML que comunicam significado semântico para tecnologias assistivas. A regra mais importante sobre ARIA é a primeira regra de ARIA: não use ARIA se existe um elemento HTML nativo que faz o trabalho. role='button' em uma div é sempre pior que um elemento button.
Dito isso, há casos legítimos onde ARIA é necessário. Widgets complexos, conteúdo dinâmico, componentes customizados. Aqui estão os atributos que uso com mais frequência e o que cada um faz.
- aria-label: fornece um label de texto para um elemento quando texto visível está ausente ou insuficiente. Use em botões apenas com ícone
- aria-labelledby: aponta para o ID de outro elemento cujo texto serve como label. Use para fieldsets de formulário e diálogos
- aria-describedby: aponta para texto descritivo adicional. Use para texto auxiliar de input e mensagens de erro
- aria-expanded: comunica o estado aberto/fechado de accordions, dropdowns e menus
- aria-live: marca uma região que receberá atualizações dinâmicas. Leitores de tela anunciarão mudanças automaticamente
- aria-hidden: esconde elementos decorativos da árvore de acessibilidade. Use em ícones dentro de botões com label
- role: sobrescreve o role ARIA implícito. Use com moderação, apenas quando o elemento semântico realmente não está disponível
// Practical ARIA in Angular templates
// Icon-only button
<button type="button" aria-label="Close notification">
<app-icon name="close" aria-hidden="true"></app-icon>
</button>
// Input with visible label and helper text
<div class="field">
<label id="email-label" for="email">Email address</label>
<input
id="email"
type="email"
aria-labelledby="email-label"
aria-describedby="email-hint email-error"
[attr.aria-invalid]="emailControl.invalid && emailControl.touched" />
<span id="email-hint">We will never share your email.</span>
<span id="email-error" role="alert" *ngIf="emailControl.invalid && emailControl.touched">
Please enter a valid email address.
</span>
</div>
// Expandable panel
<button
[id]="'btn-' + id"
[attr.aria-expanded]="isOpen"
[attr.aria-controls]="'panel-' + id"
(click)="toggle()">
{{ title }}
</button>
<div
[id]="'panel-' + id"
[hidden]="!isOpen"
role="region"
[attr.aria-labelledby]="'btn-' + id">
<ng-content></ng-content>
</div>Testes com Leitor de Tela: Você Não Pode Pular Essa Etapa
Ferramentas automatizadas como Axe, Lighthouse e IBM Equal Access Checker captam muita coisa, mas não tudo. O estudo da Deque de 2021 sobre as próprias auditorias encontrou que o teste automatizado com axe captava cerca de 57% dos problemas por volume (o número mais antigo, de 20 a 30%, se refere à cobertura de critérios de sucesso do WCAG, não ao volume de issues). O restante exige teste manual. O teste manual mais valioso é o teste com leitor de tela. Realmente desligar o monitor e tentar completar uma tarefa usando apenas áudio.
Minha configuração de teste usa NVDA no Windows com Firefox e VoiceOver no macOS com Safari. As duas estão entre as cinco combinações mais comuns da última pesquisa sobre leitores de tela publicada pelo WebAIM, embora o NVDA hoje seja usado com Chrome cerca de duas vezes mais do que com Firefox. Antes de uma feature minha ser lançada, faço um walkthrough com leitor de tela. Não é uma auditoria completa. Mas pega as falhas óbvias antes de chegarem à produção.
- NVDA + Firefox (Windows): gratuito, amplamente usado, essencial para públicos enterprise que usam Windows
- VoiceOver + Safari (macOS/iOS): embutido em dispositivos Apple, crítico para mobile
- TalkBack + Chrome (Android): essencial para testes mobile no Android
- JAWS + Chrome ou Edge (Windows): leitor de tela enterprise caro, comum em bancos e governo
Um exemplo típico: um formulário de múltiplas etapas mostra 'Etapa 1 de 5' visualmente, mas o VoiceOver lê apenas 'Formulário.' O indicador de progresso é construído inteiramente com classes CSS, sem markup semântico. Colocar um aria-label em um container comum não resolve, porque leitores de tela não anunciam de forma confiável labels em elementos genéricos. O que funciona é um texto visualmente escondido que anuncia a etapa atual, mais aria-current='step' no item ativo da lista de etapas. Esse é o tipo de problema que nenhuma ferramenta automatizada vai capturar.
O Que Eu Verifico em Cada Etapa
Verifico acessibilidade em três etapas: design review antes do handoff para dev, revisão de componente durante o desenvolvimento e uma passada pré-lançamento. No Strongest Families eu era dono das três, porque estava projetando e fazendo o handoff. No BMO eu estava do lado do desenvolvimento, trabalhando com o design system do banco, que era de outro time, então as etapas que apliquei foram a revisão de componente e a passada pré-lançamento nas minhas próprias features. O checklist da etapa de design cobre o seguinte.
- Todas as taxas de contraste de texto e elementos interativos verificadas e documentadas no Figma
- Estados de foco visíveis e estilizados para todo elemento interativo. Não apenas o outline padrão do navegador
- Estados de erro incluem tanto cor quanto um ícone ou texto. Nunca apenas cor
- Campos de formulário têm labels visíveis. Texto placeholder sozinho falha no SC 3.3.2 Labels or Instructions, e também no SC 1.3.1 quando não existe nenhum label programático
- Qualquer conteúdo transmitido apenas por cor tem um indicador secundário
- Alvos de toque atendem ao SC 2.5.8 Target Size (Minimum), 24x24 pixels CSS, que é a regra AA. No mobile eu desenho para 44x44, que é o SC 2.5.5 em AAA e bate com as diretrizes de plataforma da Apple e do Material
- Modais e diálogos têm um mecanismo explícito de fechar e um título
- Imagens têm texto alt especificado no design. Imagens decorativas são explicitamente marcadas como decorativas
- Tabelas de dados têm cabeçalhos de coluna e cabeçalhos de linha identificados
- Movimento e animação têm uma consideração de reduced-motion
O Business Case É Mais Fácil Do Que Você Imagina
Toda vez que precisei defender o investimento em acessibilidade, usei os mesmos três argumentos. Primeiro, risco legal: no Canadá e na maioria das jurisdições ocidentais, não fornecer serviços digitais acessíveis aos clientes de um banco é acionável juridicamente. Segundo, tamanho do mercado: cerca de 16% da população mundial, 1,3 bilhão de pessoas ou 1 em cada 6, vive com uma deficiência significativa segundo a OMS, e design acessível melhora a usabilidade para todos os usuários, não apenas para aqueles com deficiência. Terceiro, o efeito rampa de calçada: recursos construídos para acessibilidade, como legendas, controle por voz e atalhos de teclado, são muito usados por pessoas sem deficiência.
Acessibilidade não é uma feature que você adiciona no final. É um atributo de qualidade que você constrói desde o início, como performance ou segurança. Se você está retrofitando, já está pagando a penalidade.
O mínimo que você precisa saber não é tanto assim. Aprenda os quatro princípios WCAG. Perceptível, Operável, Compreensível, Robusto. Rode seus designs por um verificador de contraste antes do handoff. Teste com teclado antes de cada release. Teste com leitor de tela antes de cada lançamento de feature importante. Adicione ARIA apenas onde HTML nativo é insuficiente. Esse é o mínimo. Comece aí e construa para cima.
Referências
- WCAG 2.2. https://www.w3.org/TR/WCAG22/
- WebAIM Contrast Checker. https://webaim.org/resources/contrastchecker/
- NVDA screen reader. https://www.nvaccess.org/download/
- Angular CDK a11y module. https://material.angular.dev/cdk/a11y
- AODA (Ontario). https://www.ontario.ca/laws/statute/05a11
- Accessible Canada Act (S.C. 2019, c. 10). https://laws-lois.justice.gc.ca/eng/acts/A-0.6/
- World Health Organization. Disability fact sheet. https://www.who.int/news-room/fact-sheets/detail/disability-and-health
- WCAG 2.2 Understanding SC 2.5.8 Target Size (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html