Barra de progresso acessível: quem não vê a cor precisa saber quanto falta
Faça uma barra de progresso que diga o percentual em texto, anuncie o andamento ao leitor de tela sem metralhar avisos e troque a barra por uma mensagem clara quando a operação falhar.
A importação de 10.000 clientes começa e uma faixa azul cresce devagar na tela. Aos 40%, ela para. Travou ou está processando a parte pesada? Quem usa leitor de tela não sabe nem que existe uma faixa. Quem tem baixa visão vê um retângulo sem número. Quem enxerga bem fica olhando a cor e torcendo.
A resposta curta: a barra é decoração; a informação é o texto. Mostre o percentual (ou "4.200 de 10.000") escrito ao lado, use o elemento <progress> ou role="progressbar" com os atributos ARIA de valor, anuncie marcos (não cada ponto percentual) numa região aria-live e, quando não souber o fim, diga isso em vez de fingir um número.
Por que a barra bonita fica muda
Componentes prontos, como os de bibliotecas Bootstrap para Blazor, desenham uma div dentro de outra e mudam a largura da interna. Para o olho, isso é progresso. Para a árvore de acessibilidade, que é o que o leitor de tela lê, uma div sem papel nem nome não é nada. A demonstração da biblioteca mostra o controle funcionando; ela não sabe o que a sua barra mede, então não pode dar nome a ela por você.
O segundo problema é a cor. Verde para sucesso, vermelho para erro e azul para andamento só funcionam para quem distingue essas cores, num monitor calibrado, sem reflexo do sol. Cor pode reforçar uma mensagem, mas não pode ser a única forma de transmiti-la. O terceiro problema é o tempo: uma barra animada para sempre, sem número, não diferencia "falta pouco" de "travou há dez minutos".
O mínimo que uma barra precisa comunicar
- O que está sendo medido: "Importando clientes.csv", não só "Progresso".
- Quanto já foi: percentual ou contagem, em texto visível.
- Se o fim é conhecido: barra determinada com valor, ou indeterminada com a palavra "processando".
- Como terminou: mensagem de sucesso ou de erro em texto, com o que fazer a seguir.
Como fazer certo, passo a passo
- Prefira o elemento nativo
<progress>quando ele resolver. Ele já tem o papel de barra de progresso, e sem o atributovaluevira indeterminado automaticamente. - Dê um nome à barra com
<label for>,aria-labelouaria-labelledby. - Escreva o andamento em texto visível ao lado da barra. Isso serve para todo mundo, inclusive quem está no celular sob o sol.
- Se usar uma
divcustomizada, coloquerole="progressbar",aria-valuemin,aria-valuemaxearia-valuenow. Quando o valor for desconhecido, omitaaria-valuenow. - Use
aria-valuetextquando o número cru não fizer sentido, como "4.200 de 10.000 linhas". - Anuncie marcos, não cada atualização. Uma região
aria-live="polite"atualizada a cada 25%, ou a cada etapa, informa sem interromper a pessoa o tempo todo. - Na falha, troque a barra por uma mensagem com
role="alert", dizendo o que deu errado e o que a pessoa pode fazer.
A versão em HTML puro
<label for="importacao">Importando clientes.csv</label>
<progress id="importacao" max="100" value="42">42%</progress>
<p>42%: 4.200 de 10.000 linhas</p>
<!-- fim desconhecido: sem value, a barra é indeterminada -->
<label for="analise">Analisando o arquivo</label>
<progress id="analise" max="100">Processando</progress>
<p>Processando. Isso costuma levar menos de um minuto.</p>
O texto dentro de <progress> é um fallback; o que realmente garante a informação é o parágrafo visível. Não confie no conteúdo interno do elemento para quem enxerga.
Um componente Blazor que não fica mudo
No exemplo da importação, o servidor informa linhas processadas e total. O componente calcula o percentual, decide quando anunciar e trata a falha. Se a biblioteca que você usa tiver parâmetros de rótulo e texto, configure-os; se não tiver, inspecione o HTML gerado no navegador e confira se o papel e os atributos aparecem. Quando não aparecem, um componente seu, como o abaixo, é mais honesto:
@* BarraDeProgresso.razor *@
@if (Erro is not null)
{
<p role="alert">Falha na importação: @Erro</p>
}
else
{
<p id="rotulo-importacao">@Rotulo</p>
<div role="progressbar"
aria-labelledby="rotulo-importacao"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="@Percentual"
aria-valuetext="@Texto"
class="trilho">
<div class="preenchido" style="width: @(Percentual ?? 0)%"></div>
</div>
<p>@Texto</p>
<p aria-live="polite" class="somente-leitor">@anuncio</p>
}
@code {
[Parameter, EditorRequired] public string Rotulo { get; set; } = "";
[Parameter] public int? Processadas { get; set; }
[Parameter] public int? Total { get; set; }
[Parameter] public string? Erro { get; set; }
private string anuncio = "";
private int ultimoMarco = -1;
private int? Percentual => Processadas is int p && Total is int t && t > 0
? Math.Min(100, p * 100 / t)
: null;
private string Texto => Percentual is int pct
? $"{pct}%: {Processadas:N0} de {Total:N0} linhas"
: "Processando";
protected override void OnParametersSet()
{
if (Percentual is not int pct) return;
var marco = pct / 25;
if (marco != ultimoMarco)
{
ultimoMarco = marco;
anuncio = pct >= 100 ? "Importação concluída" : $"{pct}% concluído";
}
}
}
Três detalhes fazem a diferença. Quando Percentual é nulo, o Blazor não renderiza aria-valuenow, e a barra passa a ser indeterminada para a tecnologia assistiva, sem código extra. O texto visível e o aria-valuetext dizem a mesma coisa, então quem vê e quem ouve recebem a mesma informação. E o anúncio só muda a cada 25%: numa importação que atualiza a cada 50 linhas, anunciar tudo seriam 200 interrupções.
A classe somente-leitor é aquela regra de CSS que esconde o texto visualmente sem tirá-lo da árvore de acessibilidade. Não use display: none nela, porque aí o leitor de tela também ignora.
Animação, cor e o momento de terminar
Listras animadas e brilho que corre podem incomodar quem tem sensibilidade a movimento. Respeite prefers-reduced-motion no CSS e desligue a animação decorativa quando a pessoa pediu menos movimento. A barra continua avançando; só o enfeite para.
Quando a operação termina, não deixe a barra verde parada como única notícia. Escreva "10.000 clientes importados" e ofereça o próximo passo, como "Ver clientes". Se a barra some e nada aparece no lugar, quem usa leitor de tela não sabe se terminou ou se a página quebrou. O mesmo vale para upload, com uma pegadinha extra: a barra chegar a 100% significa que os bytes saíram do navegador, não que o servidor gravou o arquivo, como explica o post sobre barra de upload que não é sucesso.
Erros comuns
- Barra sem nome. O leitor de tela anuncia "barra de progresso, 42%" sem dizer do quê; com duas barras na página, a pessoa não sabe qual é qual.
- Só cor para sucesso e erro. Quem não distingue vermelho de verde vê uma barra cheia nos dois casos e acha que a importação deu certo.
- Atualizar a região
aria-livea cada 1%. O leitor fala sem parar, atropela o que a pessoa estava lendo e ela desliga o som ou abandona a tela. - Percentual inventado. Avançar até 90% por tempo e esperar ali faz a pessoa confiar num número falso e recarregar a página no pior momento.
- Animação infinita sem texto. Spinner girando por cinco minutos não diferencia lentidão de travamento.
- Remover a barra na falha sem mensagem. A tela volta ao estado inicial e a pessoa repete a operação, às vezes duplicando dados.
Checklist
- Toda barra tem um nome que diz o que está sendo medido.
- O andamento aparece em texto visível, não só na largura.
- Barras customizadas têm
role="progressbar"e os atributos de valor; as indeterminadas omitemaria-valuenow. - Os anúncios para leitor de tela acontecem em marcos, não a cada atualização.
- Sucesso e erro são comunicados em texto, com o próximo passo.
- A animação decorativa respeita
prefers-reduced-motion. - Você testou com um leitor de tela (NVDA no Windows, VoiceOver no Mac ou no iPhone) pelo menos uma vez.
Abra a tela mais demorada do seu sistema, desligue o monitor (ou ative o leitor de tela) e inicie a operação: sem olhar a cor, dá para saber quanto falta e se terminou?
Referência: https://demos.blazorbootstrap.com/progress