React Acessível
Todo app React renderiza para o mesmo HTML que o navegador sempre distribuiu, e toda ferramenta assistiva funciona a partir do DOM que seus componentes produzem. Acessibilidade em React é basicamente uma série de pequenas escolhas sobre esse DOM: qual elemento você renderiza, como ele recebe seu nome e o que você faz quando a tela muda para alguém que não consegue ver essa mudança.
Um detalhe JSX antes do resto. React renomeia class para className e for para htmlFor, mas atributos ARIA mantêm seus hífens: aria-live, aria-label, e um simples role.
Elementos semânticos em primeiro lugar
Um <button> chega com um monte de comportamento já anexado. Ele fica na ordem de tabulação, então o teclado consegue alcançá-lo. Ele ativa seu handler de clique em Enter e em Space. Um leitor de tela o anuncia como botão e lê seu texto como o nome, que também é o nome que o software de controle por voz alvo. O navegador cuida do estado desabilitado, do anel de foco e do estilo ativo.
// o navegador oferece foco, ativação por teclado e o anúncio "botão"
<button className="die" onClick={hold}>{value}</button>Uma <div> com um handler onClick recebe um item dessa lista: o clique. Tab pula por cima dela, Enter e Space não fazem nada, e um leitor de tela a lê como um fluxo de texto sem nenhuma dica de que algo vai acontecer se você interagir com ela.
O patch usual é role="button" mais tabIndex={0}, que coloca o elemento na ordem de tabulação e muda o que é anunciado. O comportamento continua faltando. Você adicionaria um handler onKeyDown, verificaria Enter e Space, chamaria preventDefault() em Space para que a página parasse de rolar, e então manteria um estado desabilitado feito à mão em sincronia com o estilo. É bem código a mais para reconstruir algo que o navegador já oferece. Usar o verdadeiro <button> é o caminho mais curto, e ele permanece correto conforme os navegadores mudam.
A mesma lógica percorre o resto da marcação: <a href> para navegação, <nav> e <main> como marcos que um leitor de tela pode pular entre eles, títulos em ordem para o contorno que as pessoas navegam. A maioria do trabalho de acessibilidade em uma base de código React é escolher o elemento que já faz o trabalho.
Anunciando o que mudou
Um app de página única se atualiza no lugar. Não há carregamento de página para dizer a um leitor de tela que algo aconteceu, então uma mudança renderizada no meio da tela pode ser completamente silenciosa. Uma região ativa passa essa informação adiante: um container que o leitor de tela vigia e anuncia sempre que seu conteúdo muda. A classe sr-only abaixo a esconde visualmente, usando um padrão CSS coberto mais tarde neste capítulo.
<div aria-live="polite" className="sr-only">
{isGameWon && <p>Você venceu! Pressione Novo Jogo para começar novamente.</p>}
</div>O wrapper renderiza toda vez, vazio no início, e React coloca um parágrafo nele quando isGameWon muda. Essa ordem é a parte que as pessoas erram. O elemento que carrega aria-live tem que estar no DOM antes do conteúdo chegar, porque leitores de tela registram regiões ativas quando as encontram e depois vigiam por mutações. Montar a região e seu texto juntos em uma única renderização e muitos leitores de tela não anunciam nada: o todo parece conteúdo novo ordinário. Manter uma região vazia na árvore não custa nada e torna o anúncio confiável.
aria-live="polite" coloca o anúncio em uma fila. O leitor de tela termina o que está lendo no momento, então entrega sua mensagem no próximo pausa natural, que pode chegar um tempo depois da mudança visual. Esse atraso é intencional, e polite é a configuração correta para quase tudo.
Interação pelo teclado
Tab move para frente pelos elementos focáveis, Shift+Tab move para trás, Enter ativa links e botões, e Space ativa botões e alterna checkboxes.
A ordem de tabulação segue a ordem do DOM, então a sequência que seu JSX renderiza é a sequência que as pessoas movem. Reordenar visualmente com CSS deixa uma ordem de tabulação que salta pela tela, e valores positivos de tabIndex causam a mesma confusão propositalmente. tabIndex={-1} é o útil: torna um elemento focável do JavaScript enquanto o mantém fora da sequência de tabulação, que é o que um alvo de foco como um título de diálogo precisa.
Mais duas regras. Mantenha o foco visível: evite outline: none a menos que um estilo :focus-visible seu próprio o substitua. E mantenha uma saída disponível: um modal que deliberadamente prende o foco dentro de si precisa de Escape para fechar e precisa devolver o foco ao seu gatilho.
Movendo foco deliberadamente
Quando a interface muda de forma, o foco pode acabar em lugar nenhum. Alguém ativa um botão, o botão é removido ou substituído, e o foco cai de volta para <body>. O próximo Tab começa no topo da página, e o leitor perdeu seu lugar.
O fix é mover o foco para algum lugar sensato, que é um dos usos legítimos de uma ref:
function NewGameButton({ isGameWon, onNewGame }) {
const buttonRef = useRef(null)
useEffect(() => {
if (isGameWon) {
buttonRef.current.focus()
}
}, [isGameWon])
return <button ref={buttonRef} onClick={onNewGame}>Novo Jogo</button>
}O effect roda depois que React confirmou aquele nó na tela, então o elemento está lá para receber foco. Guardar em isGameWon evita que ele roube foco em cada renderização.
O mesmo padrão cobre os outros momentos comuns: um diálogo leva foco ao abrir e o devolve ao gatilho ao fechar, uma falha de validação envia o foco para o primeiro campo inválido, deletar uma linha move foco para a linha que a substituiu. A regra por baixo é uma linha: se seu código removeu a coisa que tinha foco, seu código decide onde o foco vai em seguida.
Texto visualmente oculto
Bastante estado é óbvio a partir do layout e silencioso para um leitor de tela: um check verde ao lado de um campo, um dado que parece pressionado, um número que fica claro de onde está. Texto visualmente oculto explica isso para qualquer um que escute a página.
A convenção é uma classe chamada sr-only. Ela não tem significado para React ou para o navegador: é um nome de classe simples, e essas regras CSS são o que fazem o trabalho.
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}O elemento permanece na árvore de acessibilidade enquanto não ocupa espaço visual. display: none e visibility: hidden o retirariam daquela árvore também, escondendo-o de todos.
Um botão apenas com ícone é o caso do dia a dia. Ou dê a ele um aria-label, ou coloque texto real dentro e esconda-o visualmente:
<button onClick={onClose}>
<XIcon aria-hidden="true" />
<span className="sr-only">Fechar</span>
</button>aria-hidden="true" mantém o SVG decorativo fora do anúncio, e o span oculto fornece o nome. Uma ressalva sobre aria-label: ele define um nome acessível em elementos interativos e em qualquer coisa que carregue um role explícito, e navegadores frequentemente o ignoram em uma <div> ou <span> simples sem role. Mantenha-o em botões, links, inputs e marcos rotulados.
Ler o código só vai levá-lo tão longe com qualquer um desses. Ligue VoiceOver com Cmd+F5 e ouça seu próprio app, e execute axe DevTools no navegador para pegar labels faltando e controles sem nome automaticamente.
<button> real quando algo é clicável, e um <label> real ao lado de cada input. Esses elementos vêm com suporte a teclado e um nome que um leitor de tela consegue ler, tudo de graça. Quando algo muda na tela que uma pessoa escutando a página de outra forma perderia, coloque uma frase curta dentro de uma <div> com aria-live="polite", e mantenha aquela div na página desde o início para que a mudança seja notada.
Próximo: Além do básico, um mapa do que vem depois dos fundamentos.

