Componente pronto de interface: o que revisar para ele ser acessível de verdade
Revise um bloco pronto de login (shadcn/ui ou outra biblioteca) em 20 minutos: rótulo ligado ao campo, erro em texto, foco visível, contraste e ordem do Tab.
O painel da clínica ganhou um bloco de login copiado de uma biblioteca de componentes: alinhado, com a fonte certa, pronto em dez minutos. Na primeira semana, uma recepcionista que usa leitor de tela ligou dizendo que o sistema "tem dois campos sem nome", e um paciente com baixa visão não conseguia ver qual campo estava selecionado. A resposta curta: componente pronto entrega o desenho e uma base técnica; o nome de cada campo, a mensagem de erro e o contraste da sua instância continuam sendo seu trabalho. Revise cinco coisas antes de publicar: rótulo ligado, erro em texto amarrado ao campo, foco visível, contraste e ordem do Tab.
Este guia faz essa revisão no bloco de login da clínica, mostrando o que costuma vir faltando e a correção em JSX.
Por que o pacote não resolve sozinho
Bibliotecas como o shadcn/ui partem de primitivos que já tratam bem muita coisa difícil: foco dentro de um diálogo, navegação por setas num menu, atributos de estado num checkbox. Isso é valioso e você não deve reescrever. Mas o bloco de exemplo é um modelo para ser adaptado, e no shadcn/ui o código é copiado para o seu projeto, não fica num pacote fechado. A partir da cópia, cada mudança é sua.
E as mudanças acontecem. Alguém troca o rótulo por um placeholder para "economizar espaço". Alguém apaga o anel de foco porque achou feio. O tema da marca troca o cinza do texto auxiliar por um mais claro. O botão "Entrar com Google" vira só o logotipo. Nenhuma dessas mudanças quebra o visual, e todas quebram o uso para alguém.
Há ainda o que nenhum componente pode adivinhar: o texto do erro. A biblioteca não sabe que, na clínica, "credenciais inválidas" deve virar "E-mail ou senha não conferem. Confira se o Caps Lock está ligado."
A revisão em cinco pontos
- Rótulo ligado ao campo. Cada campo tem um
labelvisível cujohtmlForaponta para oiddo campo. Clicar no texto do rótulo deve colocar o cursor no campo. - Erro em texto, amarrado ao campo. A mensagem aparece em texto, perto do campo, ligada a ele por
aria-describedby, e o campo recebearia-invalid. Borda vermelha sozinha não conta. - Foco visível. Navegando com Tab, você sempre sabe onde está, em tema claro e escuro.
- Contraste. Texto comum com pelo menos 4,5:1 contra o fundo; contorno de campo e indicador de foco com pelo menos 3:1, que são as referências da WCAG.
- Ordem do Tab igual à ordem visual, e todo botão com nome que diz o que faz, inclusive os que só têm ícone.
O bloco antes da revisão
É uma versão comum depois de alguns ajustes "de design":
<form onSubmit={entrar}>
<Input type="email" placeholder="E-mail" className={erro ? "border-red-500" : ""} />
<Input type="password" placeholder="Senha" className={erro ? "border-red-500" : ""} />
<Button type="submit" className="focus-visible:ring-0">Continuar</Button>
<Button variant="outline" type="button" onClick={entrarComGoogle}>
<GoogleIcon />
</Button>
</form>
O leitor de tela anuncia os campos pelo placeholder em alguns casos e em outros não; o placeholder some assim que a pessoa digita; o erro é só a borda; o anel de foco foi zerado; o segundo botão não tem nome nenhum.
O mesmo bloco depois
<form onSubmit={entrar} noValidate>
<div>
<Label htmlFor="email">E-mail</Label>
<Input
id="email"
type="email"
autoComplete="email"
aria-invalid={erro ? true : undefined}
aria-describedby={erro ? "login-erro" : undefined}
/>
</div>
<div>
<Label htmlFor="senha">Senha</Label>
<Input
id="senha"
type="password"
autoComplete="current-password"
aria-invalid={erro ? true : undefined}
aria-describedby={erro ? "login-erro" : undefined}
/>
</div>
<p id="login-erro" role="alert">{erro}</p>
<Button type="submit">Entrar no painel</Button>
<Button variant="outline" type="button" onClick={entrarComGoogle}>
<GoogleIcon aria-hidden="true" />
Entrar com Google
</Button>
</form>
O que mudou: cada campo tem rótulo ligado; autoComplete deixa o gerenciador de senhas preencher; o erro é texto, com role="alert" para ser anunciado quando aparece, e os campos apontam para ele; o anel de foco do tema voltou porque a classe que o zerava saiu; o botão do Google tem texto e o ícone ficou escondido da árvore de acessibilidade, já que repetiria a informação. O parágrafo de erro fica sempre na página, vazio quando não há erro, porque uma região de alerta inserida junto com o texto pode não ser anunciada por todos os leitores de tela.
Contraste: confira o tema, não o exemplo
O tema padrão da biblioteca costuma passar nos testes de contraste. O seu tema, depois que a marca da clínica entrou, é outra história. Abra as ferramentas do navegador, inspecione o texto auxiliar ("Esqueceu a senha?") e veja a razão de contraste que o inspetor mostra. Faça o mesmo com o contorno dos campos, que precisa ser distinguível do fundo. No painel da clínica, o cinza do texto auxiliar estava em 3,1:1 e subiu para 5,2:1 com um tom mais escuro, sem ninguém da equipe notar diferença estética.
O teste que pega quase tudo em cinco minutos
Recarregue a página e, sem tocar no mouse, aperte Tab. O foco deve ir para o e-mail, depois senha, depois "Entrar no painel", depois "Entrar com Google", nessa ordem, sempre visível. Envie com a senha errada: o erro deve aparecer em texto e ser lido pelo leitor de tela (NVDA no Windows e VoiceOver no Mac são gratuitos). Depois ative o leitor e passe pelos campos: cada um deve ser anunciado com o nome. Por fim, aumente o zoom do navegador para 200%: nada pode ficar cortado ou sobreposto.
Se você quer ir além do login, os mesmos cinco pontos valem para qualquer bloco copiado: tabela de dados, diálogo de confirmação, menu de usuário. O post sobre formulário de login usável no teclado aprofunda o lado da experiência do login em si.
Erros comuns
- Placeholder no lugar do rótulo. O texto some quando a pessoa digita, e o campo fica sem nome para quem revisa ou usa leitor de tela.
- Erro indicado só por cor. Quem não distingue vermelho, ou não vê a tela, não sabe que algo deu errado nem o quê.
- Zerar o anel de foco para "ficar limpo". Quem navega por teclado fica cego na página; veja como manter o foco visível no tema escuro.
- Botão só com ícone. O leitor anuncia "botão" sem nome; o mesmo vale para emoji, como explica o post sobre emoji que não substitui o rótulo.
- Trocar o cinza do tema sem medir. O texto auxiliar fica ilegível em tela com brilho baixo e em quem tem visão reduzida.
- Reordenar com CSS sem mudar o HTML. A tela mostra uma ordem e o Tab percorre outra, e a pessoa pula campos sem perceber.
Checklist
- Todo campo tem rótulo visível ligado por
htmlForeid. - Mensagens de erro são texto, ligadas ao campo por
aria-describedby. - Campos com erro têm
aria-invalid. - O foco é visível em todos os elementos, nos temas claro e escuro.
- Texto tem contraste de pelo menos 4,5:1 e contornos de pelo menos 3:1.
- Botões com ícone têm texto visível ou nome acessível.
- A ordem do Tab segue a ordem visual.
- A página funciona com zoom de 200%.
A biblioteca encurtou o caminho até um login bonito, e isso é ótimo. O último trecho, que é o nome, o erro e o foco da sua instância, só você pode percorrer. Abra agora o seu formulário de login, digite uma senha errada e desligue mentalmente a cor da tela: o erro desse formulário existe fora do vermelho?
Referência: https://ui.shadcn.com/blocks/authentication