Skip to content
This page has been auto-translated and may contain errors.View in English

React Acessível

Todo app React renderiza para o mesmo HTML que o navegador sempre ofereceu, e cada ferramenta de acessibilidade 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 vê-la mudar.

Um detalhe de 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 vêm em primeiro lugar

Um <button> chega com um monte de comportamento já anexado. Fica na sequência de tabulação, então um teclado consegue alcançá-lo. Dispara seu manipulador de clique ao pressionar Enter e Space. Um leitor de tela o anuncia como botão e lê seu texto como nome, que é também o nome que software de controle de voz alvo. O navegador lida com o estado desabilitado, o anel de foco, e o estilo ativo.

jsx
// o navegador oferece foco, ativação por teclado e o anúncio "button"
<button className="die" onClick={hold}>{value}</button>

Uma <div> com um manipulador onClick recebe um item dessa lista: o clique. Tabulação pula por ela, Enter e Space não fazem nada, e um leitor de tela a lê como uma sequência de texto sem qualquer indicação de que algo acontecerá se você interagir com ela.

O remédio usual é role="button" mais tabIndex={0}, que coloca o elemento na sequência de tabulação e muda o que é anunciado. Comportamento ainda está faltando. Você adicionaria um manipulador onKeyDown, verificaria Enter e Space, chamaria preventDefault() em Space para que a página pare de rolar, e então manteria um estado desabilitado construído manualmente em sincronização com o estilo. Isso é bastante código para reconstruir algo que o navegador já oferece. Usar o <button> real é o caminho mais curto, e permanece correto conforme os navegadores mudam.

A mesma lógica funciona através do resto da marcação: <a href> para navegação, <nav> e <main> como pontos de referência entre os quais um leitor de tela pode pular, títulos em ordem para o esquema 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 mesmo lugar. Não há carregamento de página para informar um leitor de tela de que algo aconteceu, então uma mudança renderizada no meio da tela pode ser completamente silenciosa. Uma live region passa essa informação adiante: um container que o leitor de tela observa e anuncia sempre que seu conteúdo muda. A classe sr-only abaixo o oculta visualmente, usando um padrão CSS abordado posteriormente neste capítulo.

jsx
<div aria-live="polite" className="sr-only">
  {isGameWon && <p>Você venceu! Pressione Novo Jogo para começar novamente.</p>}
</div>

O wrapper renderiza a cada 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 carregando aria-live tem que estar no DOM antes do conteúdo chegar, porque leitores de tela registram live regions quando as encontram e depois observam mudanças. Monte 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, depois entrega sua mensagem na próxima pausa natural, que pode chegar um tempo depois da mudança visual. Esse atraso é deliberado, e polite é a configuração certa para quase tudo.

Interação por teclado

Tab move para frente através de elementos focáveis, Shift+Tab move para trás, Enter ativa links e botões, e Space ativa botões e alterna caixas de seleção.

A ordem de tabulação segue a ordem do DOM, então a sequência que seu JSX renderiza é a sequência pela qual as pessoas se movem. Reordenar visualmente com CSS deixa uma ordem de tabulação que salta pela tela, e valores tabIndex positivos causam a mesma confusão propositalmente. tabIndex={-1} é o útil: faz um elemento focável a partir de 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.

Duas regras a mais. 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 mantém o foco dentro de si precisa de Escape para fechar e precisa devolver o foco ao seu gatilho.

Movendo o foco deliberadamente

Quando a UI 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 volta para <body>. O próximo Tab começa no topo da página, e o leitor perdeu seu lugar.

O remédio é mover o foco para algo sensato, que é um dos usos legítimos de uma ref:

jsx
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 efeito roda depois que React confirmou aquele nó na tela, então o elemento está lá para receber o foco. Guarding em isGameWon o evita de roubar foco em cada renderização.

O mesmo padrão cobre os outros momentos comuns: um diálogo toma foco ao abrir e o devolve ao gatilho ao fechar, uma falha de validação envia 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 para onde o foco vai depois.

Texto visualmente oculto

Bastante status é óbvio a partir do layout e silencioso para um leitor de tela: um checkmark verde ao lado de um campo, um dado que parece pressionado, um número que é legível de onde está. Texto visualmente oculto soletra isso para qualquer um ouvindo a página.

A convenção é uma classe chamada sr-only. Não tem significado para React ou para o navegador: é um nome de classe simples, e essas regras CSS são o que faz o trabalho.

css
.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 removeriam dessa árvore também, ocultando-o de todos.

Um botão apenas com ícone é o caso cotidiano. Ou dê-lhe um aria-label, ou coloque texto real dentro e o oculte visualmente:

jsx
<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: define um nome acessível em elementos interativos e em qualquer coisa carregando um role explícito, e navegadores frequentemente o ignoram em uma <div> ou <span> simples sem role. Mantenha-o para botões, links, inputs, e landmarks rotulados.

Ler o código só vai levar você até certo ponto com qualquer isto. Ative VoiceOver com Cmd+F5 e ouça seu próprio app, e execute axe DevTools no navegador para pegar rótulos faltando e controles sem nome automaticamente.

Todo controle de formulário precisa de um rótulo, e formulários são onde a lacuna aparece com mais frequência. Uma <label> atada a um input dá ao campo seu nome acessível, então um leitor de tela lê "Endereço de email, editar texto" quando o foco chega lá, e o texto do rótulo se torna um alvo de clique para o campo.

Dois fiamentos funcionam. Aponte o rótulo para o input por id, usando htmlFor do React para o atributo HTML for:

jsx
<label htmlFor="email">Endereço de email</label>
<input id="email" type="email" name="email" />

Ou envolva o input no rótulo e pule o id inteiramente:

jsx
<label>
  Endereço de email
  <input type="email" name="email" />
</label>

Envolver é adequado para um checkbox ou um radio, onde o texto já fica ao lado do controle. A versão htmlFor lhe dá mais liberdade sobre o layout.

Um id codificado como email aguenta para um formulário em uma página. Alce essa marcação em um <TextField> reutilizável e duas instâncias na mesma página emitem o mesmo id, então htmlFor se liga a qual renderizou primeiro e o rótulo silenciosamente para de funcionar para cada campo depois. useId gera um id que é único por instância de componente, que é o trabalho para o qual React o adicionou:

jsx
function TextField({ label, ...props }) {
  const id = useId()

  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} {...props} />
    </>
  )
}

Sufixe esse valor para ids relacionados, ${id}-hint para um elemento de descrição, então uma chamada cobre o controle inteiro.

Texto placeholder faz um trabalho diferente. Um placeholder desaparece no instante em que alguém digita um caractere, então quando ele carrega a única descrição do campo, essa descrição desaparece no momento em que é necessário para verificar a resposta. O estilo placeholder padrão é cinza claro, que tipicamente falha em requisitos de contraste, e o suporte de leitor de tela para o atributo é inconsistente. Use-o para um exemplo do formato esperado, [email protected] sob um rótulo lendo "Endereço de email".

Texto de ajuda extra e mensagens de erro se anexam com aria-describedby, que aponta para o id do elemento mantendo o texto:

jsx
<label htmlFor="password">Senha</label>
<input
  id="password"
  type="password"
  aria-describedby="password-hint"
  aria-invalid={error ? true : undefined}
/>
<p id="password-hint">{error || 'Pelo menos 12 caracteres.'}</p>

A descrição é lida após o rótulo e o tipo de campo, então chega como contexto em vez de como o nome. aria-invalid marca o campo como falhando validação, e trocar o texto de erro no elemento que aria-describedby já aponta mantém o anúncio em um nó que o leitor de tela está rastreando. Um nível acima, um conjunto de botões radio pertence dentro de um <fieldset> com uma <legend> carregando a pergunta.

aria-live toma três valores, e a escolha decide se a região ajuda ou prejudica. off é o padrão, significando que mudanças ficam sem ser anunciadas. polite coloca o anúncio em fila e o entrega quando o leitor de tela atinge uma pausa no que já está dizendo. assertive interrompe, cortando o anúncio atual para entregar o seu. Assertive é quase sempre a escolha errada: reserve-o para algo que genuinamente bloqueia o progresso da pessoa, como uma sessão expirando em dez segundos. Uma confirmação de salvamento, uma contagem de resultado de busca, uma mudança de estado do jogo todos pertencem em uma região polite.

Dois roles carregam politeness implícita e tendem a ser anunciados mais consistentemente que um atributo aria-live simples: role="status" se comporta como polite, role="alert" como assertive, e role="status" mais aria-live="polite" é um padrão sólido para uma região de status. aria-atomic="true" então lê o conteúdo inteiro da região em qualquer mudança, que se adequa a uma frase curta que só faz sentido inteira; o padrão lê apenas o que mudou, que se adequa a um log onde cada linha fica sozinha.

O modo de falha que vale nomear é a região que anuncia demais. Ligue uma a um valor que atualiza a cada digitação, digamos uma contagem de resultado sob uma caixa de busca, e cada caractere coloca outro anúncio na fila. Entrega polite adiciona à fila em vez de substituir, então a pessoa ouve um fluxo de números antigos sobre o campo que ainda está digitando, e seu próprio eco de digitação fica enterrado. Uma bandeira de carregamento piscante ou três regiões competindo causam o mesmo acúmulo.

Então mantenha live regions poucas, debounce qualquer coisa acionada por digitação até que o valor se estabeleça, e anuncie apenas os momentos que fariam um usuário vidente olhar para cima. Um app que não diz nada é pelo menos explorável: a pessoa pode navegar com seus próprios comandos do leitor de tela em seu próprio ritmo. Um app que fala constantemente é um que eles deixam.

JunoO elemento certo faz a maior parte do trabalho Use um button real quando algo é clicável, e um label real próximo a 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 ouvindo a página de outra forma perderia, coloque uma frase curta dentro de uma div com aria-live="polite", e mantenha essa div na página desde o início para que a mudança seja percebida.
JunoO elemento certo faz a maior parte do trabalho Elementos semânticos lhe dão foco, ativação por teclado, e anúncios sem código algum, e é por isso que consertar uma div com role e tabIndex deixa você escrevendo seu próprio manipulador de teclas. Rotule cada controle com htmlFor ou um label envolvente, e trate um placeholder como uma dica de formato, já que desaparece no instante em que alguém digita. Mantenha uma região aria-live="polite" montada e troque seu texto, e mova o foco com uma ref sempre que seu código remove a coisa que tinha ele.
JunoO elemento certo faz a maior parte do trabalho Live regions são registradas quando o leitor de tela as encontra, então a região deve estar no DOM antes do conteúdo mudar, e polite entrega na próxima pausa na fala enquanto assertive interrompe e é quase sempre a escolha errada. role="status" e role="alert" carregam a mesma politeness com melhor consistência, e aria-atomic decide se a região inteira ou apenas o delta é lido. Uma região super-ansiosa acionada por digitações coloca anúncios na fila mais rápido que conseguem ser falados, o que é pior para o usuário que silêncio.

Próximo: Além o básico, um mapa do que vem depois dos fundamentos.