Acessibilidade (a11y) no frontend moderno
1. Fundamentos da Acessibilidade Web
Acessibilidade web, abreviada como a11y (a + 11 letras + y), é a prática de desenvolver sites e aplicações que possam ser utilizados por todas as pessoas, independentemente de suas capacidades físicas ou cognitivas. No desenvolvimento moderno com JavaScript, Node.js e React, a acessibilidade não é opcional — é um requisito fundamental que impacta diretamente a experiência do usuário, a conformidade legal e o SEO.
As WCAG (Web Content Accessibility Guidelines) estabelecem quatro princípios fundamentais: Percebível, Operável, Compreensível e Robusto (POUR). Cada princípio possui critérios de sucesso organizados em três níveis de conformidade: A (mínimo), AA (recomendado) e AAA (avançado). Para a maioria dos projetos, o nível AA é o alvo prático.
Ferramentas de auditoria automatizada como Lighthouse (integrado ao Chrome DevTools), axe DevTools e WAVE ajudam a identificar problemas rapidamente, mas não substituem testes manuais com leitores de tela.
2. HTML Semântico e ARIA no React
A base da acessibilidade começa com HTML semântico. Elementos como <nav>, <main>, <aside> e <footer> criam landmarks que leitores de tela utilizam para navegação rápida.
function Layout({ children }) {
return (
<>
<nav aria-label="Navegação principal">
<a href="/">Home</a>
</nav>
<main id="conteudo-principal">
{children}
</main>
<aside aria-label="Barra lateral">
<h2>Artigos relacionados</h2>
</aside>
</>
);
}
ARIA (Accessible Rich Internet Applications) deve ser usado com cautela. A regra de ouro: não use ARIA se o HTML nativo resolver o problema. Por exemplo, um <button> nativo já é acessível por teclado e leitores de tela, enquanto um <div> com role="button" exige implementação manual de eventos de teclado.
// ❌ Evite: reinventar a roda sem necessidade
function BadButton({ onClick, children }) {
return (
<div role="button" tabIndex={0} onClick={onClick}>
{children}
</div>
);
}
// ✅ Prefira: elemento nativo
function GoodButton({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
3. Navegação por Teclado e Foco Gerenciado
O gerenciamento de foco é crítico em SPAs. Use useRef e focus() programático para direcionar o foco após transições de rota ou abertura de modais.
import { useRef, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function SkipLink() {
const mainRef = useRef(null);
const location = useLocation();
useEffect(() => {
mainRef.current?.focus();
}, [location]);
return (
<>
<a href="#conteudo" className="skip-link">
Pular para o conteúdo principal
</a>
<main ref={mainRef} tabIndex={-1} id="conteudo">
<h1>Página atual</h1>
</main>
</>
);
}
Roving tabindex é uma técnica para navegação por setas em listas. Apenas um elemento mantém tabIndex={0}, enquanto os demais têm tabIndex={-1}:
function CardList({ items }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e) => {
if (e.key === 'ArrowDown') {
setActiveIndex((prev) => Math.min(prev + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
setActiveIndex((prev) => Math.max(prev - 1, 0));
}
};
return (
<ul role="listbox" onKeyDown={handleKeyDown}>
{items.map((item, index) => (
<li
key={item.id}
role="option"
tabIndex={index === activeIndex ? 0 : -1}
aria-selected={index === activeIndex}
>
{item.title}
</li>
))}
</ul>
);
}
4. Componentes Acessíveis com React Hooks
O hook useId (React 18+) gera identificadores únicos, essenciais para associar labels e descrições:
import { useId, useState } from 'react';
function Accordion({ title, children }) {
const id = useId();
const [expanded, setExpanded] = useState(false);
return (
<div>
<button
aria-expanded={expanded}
aria-controls={id}
onClick={() => setExpanded(!expanded)}
>
{title}
</button>
<div id={id} role="region" hidden={!expanded}>
{children}
</div>
</div>
);
}
Para trap focus em modais, combine useEffect com um callback que restringe o foco ao container:
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
modalRef.current?.focus();
const handleTab = (e) => {
const focusable = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" ref={modalRef}>
<button onClick={onClose}>Fechar</button>
{children}
</div>
);
}
5. Formulários Acessíveis e Validação
Labels explícitos com htmlFor são preferíveis. Use aria-describedby para associar mensagens de erro:
import { useId, useState } from 'react';
function LoginForm() {
const emailId = useId();
const errorId = useId();
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!email.includes('@')) {
setError('Email inválido');
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor={emailId}>Email</label>
<input
id={emailId}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
{error && (
<p id={errorId} role="alert" style={{ color: 'red' }}>
{error}
</p>
)}
<button type="submit">Enviar</button>
</form>
);
}
Com React Hook Form, a validação em tempo real fica mais limpa:
import { useForm } from 'react-hook-form';
function FormAcessivel() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<label htmlFor="nome">Nome completo</label>
<input
id="nome"
{...register('nome', { required: 'Campo obrigatório' })}
aria-invalid={!!errors.nome}
aria-describedby={errors.nome ? 'erro-nome' : undefined}
/>
{errors.nome && (
<p id="erro-nome" role="alert">{errors.nome.message}</p>
)}
</form>
);
}
6. Testes de Acessibilidade Automatizados e Manuais
Com jest-axe e @testing-library/react, é possível testar violações automaticamente:
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('botão não deve ter violações de acessibilidade', async () => {
const { container } = render(<button>Clique aqui</button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Para testes de navegação por teclado com Cypress ou Playwright:
// Exemplo com Cypress
cy.visit('/');
cy.get('body').tab(); // Avança para o próximo elemento focável
cy.focused().should('have.text', 'Pular para conteúdo');
Testes manuais essenciais:
- Zoom para 200% — o layout deve permanecer funcional
- Modo de alto contraste do sistema operacional
- Navegação completa apenas com teclado (Tab, Enter, Espaço, Setas)
7. Performance e Acessibilidade no Ecossistema Node.js
No Next.js, o SSR permite gerar HTML acessível desde o primeiro carregamento:
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="pt-BR">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Lazy loading de imagens com descrição adequada:
function Galeria({ imagens }) {
return (
<div>
{imagens.map((img) => (
<img
key={img.id}
src={img.url}
alt={img.descricao}
loading="lazy"
width={400}
height={300}
/>
))}
</div>
);
}
Para internacionalização (i18n), um middleware no Node.js pode detectar o idioma preferido:
// middleware.js (Next.js)
import { NextResponse } from 'next/server';
export function middleware(request) {
const acceptLang = request.headers.get('accept-language');
const lang = acceptLang?.startsWith('pt') ? 'pt-BR' : 'en';
return NextResponse.redirect(new URL(`/${lang}`, request.url));
}
Referências
- Web Content Accessibility Guidelines (WCAG) 2.2 — Documentação oficial da W3C com todos os critérios de sucesso e técnicas de implementação.
- React Acessibilidade - Documentação Oficial — Guia do React sobre fragmentos, labels, eventos e foco em componentes acessíveis.
- axe DevTools - Browser Extension — Extensão para Chrome e Firefox que realiza auditoria automatizada de acessibilidade diretamente no navegador.
- Testing Accessibility with jest-axe — Repositório oficial do jest-axe com exemplos de integração em testes unitários com Jest.
- Next.js Accessibility — Documentação do Next.js sobre boas práticas de acessibilidade em aplicações SSR e estáticas.
- A Complete Guide to Accessible Front-End Components — Artigo da Smashing Magazine com implementações detalhadas de componentes acessíveis em React.
- WAVE Web Accessibility Evaluation Tool — Ferramenta online da WebAIM para identificar erros de acessibilidade em páginas web.