Parte da nossa série Compliance & Regulation
Leia o guia completoAcessibilidade na Web: Guia de conformidade WCAG 2.1 AA
Acessibilidade não é um recurso que você adiciona após o lançamento – é um atributo fundamental de qualidade, assim como desempenho ou segurança. A conformidade com WCAG 2.1 AA é agora legalmente exigida na UE (Lei Europeia de Acessibilidade, aplicada em junho de 2025), nos EUA (jurisprudência do Título III da ADA) e em muitas outras jurisdições. Além da conformidade, as interfaces acessíveis convertem melhor, têm uma classificação mais elevada nas pesquisas e atendem cerca de 1,3 bilhão de pessoas com deficiência em todo o mundo.
Este guia é um manual prático de implementação e não uma lista de verificação. Você aprenderá os quatro princípios WCAG, as técnicas mais impactantes, como testar sistematicamente e como integrar a acessibilidade em seu fluxo de trabalho de desenvolvimento React/Next.js para que permaneça fixo.
Principais conclusões
- WCAG 2.1 AA exige todos os quatro princípios POUR: Perceptível, Operável, Compreensível, Robusto
- Comece com HTML semântico — ele fornece 70% de acessibilidade gratuitamente antes de qualquer ARIA ser adicionado
- Relação mínima de contraste de cores: 4,5:1 para texto normal, 3:1 para texto grande (18pt/14pt em negrito)
- Cada elemento interativo deve ser focalizável pelo teclado com um indicador de foco visível
- Os leitores de tela anunciam com base na árvore de acessibilidade — teste com NVDA (Windows) e VoiceOver (Mac)
- ARIA é um último recurso - apenas muda a forma como as tecnologias assistivas interpretam o DOM, não o comportamento
- Automatize com axe-core em seu pipeline de CI; o teste manual detecta o que a automação perde
- Documente sua declaração de acessibilidade e forneça um mecanismo de feedback para os usuários relatarem problemas
Os Quatro Princípios POUR
As WCAG 2.1 estão organizadas em torno de quatro princípios. Cada critério de sucesso pertence a um deles.
Perceptível: as informações devem ser apresentáveis de forma que os usuários possam percebê-las. Isso abrange alternativas de texto para imagens, legendas para vídeo, contraste de cores suficiente e conteúdo que não depende apenas da cor para transmitir significado.
Operável: todas as funcionalidades devem ser operáveis via teclado, com tempo suficiente para interagir, sem conteúdo que desencadeie convulsões e com estrutura navegável (pular links, títulos de páginas, ordem de foco).
Compreensível: o conteúdo deve ser legível e previsível. O idioma deve ser identificado, as mensagens de erro devem ser descritivas e os formulários devem ter rótulos claros e feedback de validação.
Robusto: o conteúdo deve ser interpretável por tecnologias assistivas atuais e futuras. Isso significa HTML válido, uso adequado de ARIA e mensagens de status anunciadas sem necessidade de foco.
HTML semântico primeiro
HTML semântico é o investimento único em acessibilidade de maior aproveitamento. Os elementos HTML nativos vêm com funções de acessibilidade, estados e comportamento de teclado integrados – sem necessidade de ARIA.
// BAD: Generic divs with no semantics
<div class="button" onclick="submit()">Submit</div>
<div class="nav">
<div class="link" onclick="navigate('/home')">Home</div>
</div>
// GOOD: Native semantics, free keyboard and screen reader support
<button type="submit" onClick={submit}>Submit</button>
<nav aria-label="Main navigation">
<a href="/home">Home</a>
</nav>
As regiões de referência ajudam os usuários de leitores de tela a navegar rapidamente, saltando entre as seções:
// Every page should have these landmarks
<header> {/* banner landmark */}
<nav aria-label="Main">...</nav>
</header>
<main> {/* main landmark */}
<h1>Page Title</h1>
<article>...</article>
<aside aria-label="Related content">...</aside>
</main>
<footer> {/* contentinfo landmark */}
<nav aria-label="Footer">...</nav>
</footer>
A hierarquia de títulos deve ser lógica e ininterrupta:
// BAD: Skipped heading levels
<h1>Page Title</h1>
<h3>Section</h3> {/* Skipped h2! */}
// GOOD: Sequential hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
Contraste de cores
WCAG 2.1 AA requer:
- 4,5:1 relação de contraste para texto normal (abaixo de 18pt/14pt em negrito)
- Relação de contraste 3:1 para texto grande (18pt+ / 14pt+ negrito)
- 3:1 para componentes de UI e objetos gráficos (botões, ícones, bordas de entrada)
// Tailwind color contrast examples
// FAIL: gray-400 on white (#9ca3af on #fff = 2.8:1)
<p className="text-gray-400">This fails AA</p>
// PASS: gray-700 on white (#374151 on #fff = 10.7:1)
<p className="text-gray-700">This passes AA</p>
// For dark mode, test both themes separately
<p className="text-gray-700 dark:text-gray-300">
gray-700 on white (10.7:1) / gray-300 on gray-900 (9.2:1)
</p>
Use o WebAIM Contrast Checker ou a ferramenta de contraste DevTools do navegador durante o desenvolvimento. Adicione isto ao seu Storybook ou sistema de tokens de design para capturar regressões:
// contrast-checker.ts
import { getContrast } from 'polished';
function assertContrast(fg: string, bg: string, level: 'AA' | 'AAA' = 'AA') {
const ratio = getContrast(fg, bg);
const required = level === 'AA' ? 4.5 : 7;
if (ratio < required) {
throw new Error(
`Contrast ratio ${ratio.toFixed(2)}:1 fails WCAG ${level} (requires ${required}:1)`
);
}
}
Navegação pelo teclado
Cada elemento interativo – links, botões, campos de formulário, widgets personalizados – deve ser acessível e operável via teclado.
Gerenciamento de foco
// Skip link: first element on every page
// Allows keyboard users to jump past navigation
export function SkipLink() {
return (
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4
focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white
focus:rounded focus:ring-2 focus:ring-white"
>
Skip to main content
</a>
);
}
// Main content target
<main id="main-content" tabIndex={-1}>
{/* tabIndex={-1} allows programmatic focus without appearing in tab order */}
Focus Trapping em modais
Quando uma caixa de diálogo é aberta, o foco deve estar preso dentro dela. Quando fechado, o foco retorna ao gatilho:
// focus-trap.tsx using @radix-ui/react-focus-trap (used internally by shadcn Dialog)
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useRef } from 'react';
export function AccessibleModal({ trigger, children, title }: Props) {
const triggerRef = useRef<HTMLButtonElement>(null);
return (
<Dialog>
<DialogTrigger ref={triggerRef} asChild>
<button>Open</button>
</DialogTrigger>
<DialogContent
// shadcn Dialog handles focus trap and returns focus to trigger on close
aria-describedby="dialog-description"
>
<DialogTitle>{title}</DialogTitle>
<p id="dialog-description" className="sr-only">
{/* Screen reader description of dialog purpose */}
</p>
{children}
</DialogContent>
</Dialog>
);
}
Indicadores de foco visíveis
WCAG 2.1 SC 2.4.11 (AA em WCAG 2.2) requer um contorno de foco mínimo de 2px. Nunca suprima o foco sem um substituto:
/* globals.css */
/* NEVER do this: */
:focus { outline: none; }
/* DO this: custom, visible focus ring */
:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
border-radius: 4px;
}
/* Remove for mouse users (only show for keyboard) */
:focus:not(:focus-visible) {
outline: none;
}
ARIA: quando e como usá-lo
Os atributos ARIA (Accessible Rich Internet Applications) modificam a forma como as tecnologias assistivas interpretam o DOM. A primeira regra do ARIA: não use se existir um elemento HTML nativo para o seu caso de uso.
Etiquetas ARIA
// Icon-only button — screen reader has nothing to announce without aria-label
<button aria-label="Close dialog">
<X className="h-4 w-4" aria-hidden="true" />
</button>
// Form field with visible label — use htmlFor, not aria-label
<label htmlFor="email">Email address</label>
<input id="email" type="email" />
// Input with visible description
<input
id="password"
type="password"
aria-describedby="password-requirements"
/>
<p id="password-requirements">Must be at least 12 characters.</p>
Regiões ativas ARIA
Anuncie mudanças dinâmicas de conteúdo sem mudar o foco:
// Status messages (search results count, form submission status)
function SearchResults({ count, loading }: Props) {
return (
<>
{/* aria-live="polite" waits for user to finish current action */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{loading ? 'Loading results...' : `${count} results found`}
</div>
{/* Visual result count (not for screen readers — aria-hidden) */}
<span aria-hidden="true">{count} results</span>
</>
);
}
// Error messages (aria-live="assertive" interrupts immediately)
function FormError({ error }: { error: string | null }) {
return (
<div
role="alert"
aria-live="assertive"
className={cn('text-red-500 text-sm', !error && 'hidden')}
>
{error}
</div>
);
}
ARIA para widgets personalizados
Quando você precisar construir um widget personalizado (painel de guias, visualização em árvore, caixa de combinação), siga exatamente os padrões do ARIA Authoring Practices Guide (APG):
// Accessible tabs (ARIA tab pattern)
export function Tabs({ items }: { items: Tab[] }) {
const [active, setActive] = useState(0);
return (
<div>
<div role="tablist" aria-label="Content tabs">
{items.map((item, i) => (
<button
key={item.id}
role="tab"
aria-selected={active === i}
aria-controls={`panel-${item.id}`}
id={`tab-${item.id}`}
tabIndex={active === i ? 0 : -1} // Roving tabindex
onClick={() => setActive(i)}
onKeyDown={(e) => {
if (e.key === 'ArrowRight') setActive((active + 1) % items.length);
if (e.key === 'ArrowLeft') setActive((active - 1 + items.length) % items.length);
}}
>
{item.label}
</button>
))}
</div>
{items.map((item, i) => (
<div
key={item.id}
role="tabpanel"
id={`panel-${item.id}`}
aria-labelledby={`tab-${item.id}`}
hidden={active !== i}
>
{item.content}
</div>
))}
</div>
);
}
Formulários e tratamento de erros
Os formulários acessíveis estão entre as melhorias de maior impacto para usuários com deficiências cognitivas e motoras.
// Accessible form field with error state
function TextField({
id,
label,
error,
required,
hint,
...props
}: TextFieldProps) {
const hintId = hint ? `${id}-hint` : undefined;
const errorId = error ? `${id}-error` : undefined;
const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;
return (
<div>
<label htmlFor={id} className="font-medium">
{label}
{required && <span aria-hidden="true" className="text-red-500 ml-1">*</span>}
{required && <span className="sr-only">(required)</span>}
</label>
{hint && (
<p id={hintId} className="text-sm text-gray-500 mt-1">
{hint}
</p>
)}
<input
id={id}
aria-required={required}
aria-invalid={!!error}
aria-describedby={describedBy}
className={cn('input', error && 'border-red-500')}
{...props}
/>
{error && (
<p id={errorId} className="text-sm text-red-500 mt-1" role="alert">
{error}
</p>
)}
</div>
);
}
Imagens e mídia
// Informative image
<img src="/chart.png" alt="Bar chart showing 40% revenue growth in Q4 2025" />
// Decorative image — empty alt hides it from screen readers
<img src="/divider.png" alt="" role="presentation" />
// Complex image — use aria-describedby for long descriptions
<figure>
<img
src="/architecture.png"
alt="System architecture diagram"
aria-describedby="arch-desc"
/>
<figcaption id="arch-desc">
The diagram shows three tiers: frontend Next.js on port 3000,
NestJS API on port 3001, and PostgreSQL database on port 5433.
Redis sits between the API and database layers.
</figcaption>
</figure>
// SVG icons used as decoration
<svg aria-hidden="true" focusable="false">
<use href="#icon-search" />
</svg>
Teste automatizado com axe-core
pnpm add -D @axe-core/playwright axe-core
// tests/a11y/homepage.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Homepage accessibility', () => {
test('should have no WCAG 2.1 AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('#third-party-widget') // Exclude known external violations
.analyze();
expect(results.violations).toEqual([]);
});
test('should be keyboard navigable', async ({ page }) => {
await page.goto('/');
// Tab through interactive elements and verify focus is visible
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(
() => document.activeElement?.getAttribute('href')
);
expect(focusedElement).toBe('#main-content'); // Skip link
});
});
Adicionar ao pipeline de CI:
# .github/workflows/ci.yml
- name: Run accessibility tests
run: cd apps/web && npx playwright test tests/a11y --reporter=html
- uses: actions/upload-artifact@v4
with:
name: a11y-report
path: apps/web/playwright-report/
Perguntas frequentes
Qual é a diferença entre WCAG 2.1 A, AA e AAA?
O Nível A é o mínimo – falhar no Nível A significa que o conteúdo é inacessível para alguns usuários de maneiras fundamentais. O nível AA é o padrão legal na maioria das jurisdições e atende às necessidades mais amplas dos usuários. O nível AAA é aspiracional – alguns critérios não podem ser atendidos para todos os tipos de conteúdo. Almeje a conformidade com AA como sua linha de base e busque AAA sempre que possível.
Usar uma biblioteca de componentes como shadcn/ui torna meu aplicativo acessível?
shadcn/ui é construído em primitivas de UI Radix, que são acessíveis por design – elas incluem funções ARIA corretas, navegação por teclado e gerenciamento de foco. No entanto, você ainda precisa adicionar rótulos significativos, lidar com estados de erro de forma acessível, garantir contraste de cores suficiente com seu tema personalizado e testar com tecnologias assistivas reais. As bibliotecas de componentes reduzem a carga, mas não eliminam a necessidade de testes de acessibilidade.
Como faço para testar com um leitor de tela?
No Windows, use o NVDA (gratuito) com Firefox ou Chrome. No macOS, use o VoiceOver (integrado, Cmd+F5) com Safari. No celular, use TalkBack (Android) ou VoiceOver (iOS). Teste as principais jornadas do usuário: preenchimento de formulários, interações modais, navegação por pontos de referência e leitura de conteúdo dinâmico. O teste do leitor de tela detecta anúncios, ordem de leitura e comportamento de foco que as ferramentas automatizadas não percebem.
Qual é o padrão de tabindex itinerante?
Roving tabindex é o padrão de teclado para widgets compostos (listas de guias, barras de ferramentas, grupos de rádio, visualizações em árvore). Apenas um item do grupo possui tabIndex={0} por vez — o item ativo. Todos os outros recebem tabIndex={-1}. As teclas de seta movem o foco dentro do grupo e atualizam qual item tem tabIndex 0. Isso evita que o usuário navegue por todos os itens do grupo - eles entram no grupo com Tab, navegam com as teclas de seta e saem com Tab.
Como lidar com a acessibilidade de conteúdo dinâmico carregado via AJAX?
Use regiões aria-live para atualizações de status (contagem de resultados de pesquisa, salvamento de confirmações). Para substituições de seções de páginas inteiras, mova o foco para o título ou contêiner do novo conteúdo após o carregamento. Para carregar estados, use aria-busy="true" na região que está sendo atualizada e uma região aria-live="polite" para anunciar a conclusão. Sempre teste com um leitor de tela para verificar se os anúncios são claros e oportunos.
Próximas etapas
A acessibilidade na Web é uma prática contínua, não uma auditoria única. Comece corrigindo seu HTML semântico e contraste de cores, depois coloque a navegação no teclado e ARIA para widgets complexos e automatize a validação WCAG em seu pipeline de CI para capturar regressões.
ECOSIRE constrói aplicativos da web compatíveis com WCAG 2.1 AA como um padrão básico em cada projeto. Se você precisar de uma auditoria de acessibilidade ou quiser criar conformidade desde o início, explore nossos serviços de engenharia de front-end.
Escrito por
ECOSIRE Research and Development Team
Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.
Artigos Relacionados
Audit Preparation Checklist: Getting Your Books Ready
Complete audit preparation checklist covering financial statement readiness, supporting documentation, internal controls documentation, auditor PBC lists, and common audit findings.
Australian GST Guide for eCommerce Businesses
Complete Australian GST guide for eCommerce businesses covering ATO registration, the $75,000 threshold, low value imports, BAS lodgement, and GST for digital services.
Canadian HST/GST Guide: Province-by-Province
Complete Canadian HST/GST guide covering registration requirements, province-by-province rates, input tax credits, QST, place of supply rules, and CRA compliance.
Mais de Compliance & Regulation
Audit Preparation Checklist: Getting Your Books Ready
Complete audit preparation checklist covering financial statement readiness, supporting documentation, internal controls documentation, auditor PBC lists, and common audit findings.
Australian GST Guide for eCommerce Businesses
Complete Australian GST guide for eCommerce businesses covering ATO registration, the $75,000 threshold, low value imports, BAS lodgement, and GST for digital services.
Canadian HST/GST Guide: Province-by-Province
Complete Canadian HST/GST guide covering registration requirements, province-by-province rates, input tax credits, QST, place of supply rules, and CRA compliance.
Healthcare Accounting: Compliance and Financial Management
Complete guide to healthcare accounting covering HIPAA financial compliance, contractual adjustments, charity care, cost report preparation, and revenue cycle management.
India GST Compliance for Digital Businesses
Complete India GST compliance guide for digital businesses covering registration, GSTIN, rates, input tax credits, e-invoicing, GSTR returns, and TDS/TCS provisions.
Fund Accounting for Nonprofits: Best Practices
Master nonprofit fund accounting with net asset classifications, grant tracking, Form 990 preparation, functional expense allocation, and audit readiness best practices.