Como criar um componente da barra de carregamento

Uma visão geral básica de como criar uma barra de carregamento acessível e adaptável a cores com o elemento <progress>.

Nesta postagem, quero compartilhar ideias sobre como criar uma barra de carregamento acessível e adaptável a cores com o elemento <progress>. Teste a demonstração e confira a fonte.

Demonstração de claro e escuro, indeterminado, crescente e de conclusão no Chrome.

Se você prefere vídeos, confira a versão deste post no YouTube:

Visão geral

O elemento <progress> fornece feedback visual e audível aos usuários sobre a conclusão. Esse feedback visual é útil para cenários como: progresso em um formulário, exibição de informações de download ou upload ou até mesmo para mostrar que o progresso é desconhecido, mas o trabalho ainda está ativo.

Este desafio de GUI trabalhou com o elemento HTML <progress> para facilitar a acessibilidade. As cores e os layouts ampliam os limites de personalização do elemento integrado para modernizar o componente e fazer com que ele se encaixe melhor nos sistemas de design.

Guias claras e escuras em cada navegador que mostram uma 
    visão geral do ícone adaptativo de cima para baixo: 
    Safari, Firefox, Chrome.
Demonstração mostrada no Firefox, Safari, Safari para iOS, Chrome e Chrome para Android em esquemas claros e escuros.

Marcação

Escolhi encapsular o elemento <progress> em um <label> para poder pular os atributos de relacionamento explícitos em favor de um relacionamento implícito. Também rotulei um elemento pai afetado pelo estado de carregamento para que as tecnologias de leitor de tela possam transmitir essas informações ao usuário.

<progress></progress>

Se não houver value, o progresso do elemento será indeterminado. O atributo max tem o valor padrão 1, então o progresso fica entre 0 e 1. Definir max como 100, por exemplo, definiria o intervalo como 0-100. Escolhi ficar dentro dos limites de 0 e 1, traduzindo os valores de progresso para 0,5 ou 50%.

Progresso com rótulo

Em uma relação implícita, um elemento de progresso é envolvido por um rótulo como este:

<label>Loading progress<progress></progress></label>

Na minha demonstração, escolhi incluir o rótulo somente para leitores de tela. Para fazer isso, coloque o texto do rótulo em um <span> e aplique alguns estilos para que ele fique fora da tela:

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

Com o seguinte CSS do WebAIM (link em inglês):

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

Captura de tela das ferramentas para desenvolvedores mostrando o elemento somente para leitores de tela.

Área afetada pelo progresso do carregamento

Se você tem uma visão saudável, pode ser fácil associar um indicador de progresso a elementos e áreas de página relacionados, mas para usuários com deficiência visual, isso não é tão claro. Para melhorar isso, atribua o atributo aria-busy ao elemento mais alto que vai mudar quando o carregamento for concluído. Além disso, indique uma relação entre o progresso e a zona de carregamento com aria-describedby.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

No JavaScript, alterne aria-busy para true no início da tarefa e para false quando terminar.

Adições de atributos ARIA

Embora o papel implícito de um elemento <progress> seja progressbar, eu o tornei explícito para navegadores que não têm esse papel implícito. Também adicionei o atributo indeterminate para colocar explicitamente o elemento em um estado desconhecido, o que é mais claro do que observar que o elemento não tem um value definido.

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

Use tabindex="-1" para tornar o elemento de progresso focalizável no JavaScript. Isso é importante para a tecnologia de leitores de tela, já que dar foco ao progresso conforme ele muda vai anunciar ao usuário até onde o progresso atualizado chegou.

Estilos

O elemento de progresso é um pouco complicado quando se trata de estilização. Os elementos HTML integrados têm partes ocultas especiais que podem ser difíceis de selecionar e, muitas vezes, oferecem apenas um conjunto limitado de propriedades a serem definidas.

Layout

Os estilos de layout permitem certa flexibilidade no tamanho do elemento de progresso e na posição do rótulo. Um estado de conclusão especial é adicionado, o que pode ser uma dica visual extra útil, mas não obrigatória.

Layout do <progress>

A largura do elemento de progresso não é alterada para que ele possa diminuir e aumentar com o espaço necessário no design. Os estilos integrados são removidos definindo appearance e border como none. Isso é feito para que o elemento possa ser normalizado em todos os navegadores, já que cada um tem os próprios estilos para o elemento.

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

O valor de 1e3px para _radius usa a notação científica de números para expressar um número grande, de modo que o border-radius seja sempre arredondado. É equivalente a 1000px. Gosto de usar isso porque meu objetivo é usar um valor grande o suficiente para que eu possa definir e esquecer (e é mais curto para escrever do que 1000px). Também é fácil aumentá-lo ainda mais, se necessário: basta mudar o 3 para 4, então 1e4px é equivalente a 10000px.

overflow: hidden é usado e tem sido um estilo controverso. Isso facilitou algumas coisas, como não precisar transmitir valores de border-radius para os elementos de faixa e de preenchimento de faixa. No entanto, isso também significava que nenhum filho do progresso poderia ficar fora do elemento. Outra iteração nesse elemento de progresso personalizado pode ser feita sem overflow: hidden e pode abrir algumas oportunidades para animações ou estados de conclusão melhores.

Progresso concluído

Os seletores de CSS fazem o trabalho difícil aqui, comparando o máximo com o valor. Se eles forem iguais, o progresso estará concluído. Quando concluído, um pseudoelemento é gerado e anexado ao final do elemento de progresso, fornecendo uma boa indicação visual adicional da conclusão.

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

Captura de tela da barra de carregamento em 100% e mostrando uma marca de seleção no final.

Cor

O navegador traz as próprias cores para o elemento de progresso e se adapta aos modos claro e escuro com apenas uma propriedade CSS. Isso pode ser criado com alguns seletores específicos do navegador.

Estilos de navegador claro e escuro

Para ativar um elemento <progress> adaptável ao tema escuro e claro no seu site, basta usar color-scheme.

progress {
  color-scheme: light dark;
}

Cor de preenchimento do progresso de uma única propriedade

Para colorir um elemento <progress>, use accent-color.

progress {
  accent-color: rebeccapurple;
}

Observe que a cor do plano de fundo da faixa muda de claro para escuro, dependendo do accent-color. O navegador está garantindo o contraste adequado: bem legal.

Cores claras e escuras totalmente personalizadas

Defina duas propriedades personalizadas no elemento <progress>, uma para a cor da faixa e outra para a cor do progresso da faixa. Dentro da consulta de mídia prefers-color-scheme, forneça novos valores de cor para a faixa e o progresso da faixa.

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

Estilos de foco

Anteriormente, demos ao elemento um índice de tabulação negativo para que ele pudesse ser focado de forma programática. Use :focus-visible para personalizar o foco e ativar o estilo de anel de foco mais inteligente. Com isso, um clique e um foco do mouse não vão mostrar o anel de foco, mas os cliques do teclado sim. O vídeo do YouTube aborda isso com mais detalhes e vale a pena assistir.

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

Captura de tela da barra de carregamento com um anel de foco ao redor. As cores são todas iguais.

Estilos personalizados em navegadores

Personalize os estilos selecionando as partes de um elemento <progress> que cada navegador expõe. O elemento de progresso é uma única tag, mas é composto por alguns elementos filhos que são expostos por pseudosseletores de CSS. O Chrome DevTools vai mostrar esses elementos se você ativar a configuração:

  1. Clique com o botão direito do mouse na página e selecione Inspecionar elemento para abrir o DevTools.
  2. Clique na engrenagem de configurações no canto superior direito da janela do DevTools.
  3. No cabeçalho Elementos, encontre e marque a caixa de seleção Mostrar DOM de sombra do user agent.

Captura de tela de onde ativar a exposição do DOM de sombra do user agent no DevTools.

Estilos do Safari e do Chromium

Navegadores baseados em WebKit, como o Safari e o Chromium, expõem ::-webkit-progress-bar e ::-webkit-progress-value, que permitem o uso de um subconjunto de CSS. Por enquanto, defina background-color usando as propriedades personalizadas criadas anteriormente, que se adaptam aos temas claro e escuro.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

Captura de tela mostrando os elementos internos do elemento de progresso.

Estilos do Firefox

O Firefox só expõe o pseudoseletor ::-moz-progress-bar no elemento <progress>. Isso também significa que não podemos colorir a faixa diretamente.

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

Captura de tela do Firefox e onde encontrar as partes do elemento de progresso.

Captura de tela do Debugging Corner em que o Safari, o Safari para iOS,
  o Firefox, o Chrome e o Chrome para Android mostram a barra de carregamento funcionando.

O Firefox tem uma cor de faixa definida em accent-color, enquanto o Safari do iOS tem uma faixa azul-clara. O mesmo acontece no modo escuro: o Firefox tem uma faixa escura, mas não a cor personalizada que definimos, e funciona em navegadores baseados em Webkit.

Animação

Ao trabalhar com pseudoseletores integrados ao navegador, geralmente é com um conjunto limitado de propriedades CSS permitidas.

Como animar o preenchimento da barra de progresso

Adicionar uma transição ao inline-size do elemento de progresso funciona para o Chromium, mas não para o Safari. O Firefox também não usa uma propriedade de transição no ::-moz-progress-bar.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

Como animar o estado :indeterminate

Aqui, eu uso um pouco mais de criatividade para fornecer uma animação. Um pseudoelemento para o Chromium é criado, e um gradiente é aplicado e animado para frente e para trás nos três navegadores.

As propriedades personalizadas

As propriedades personalizadas são ótimas para muitas coisas, mas uma das minhas favoritas é simplesmente dar um nome a um valor CSS que, de outra forma, pareceria mágico. Veja a seguir um linear-gradient bastante complexo, mas com um nome legal. A finalidade e os casos de uso podem ser entendidos com clareza.

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

As propriedades personalizadas também ajudam o código a permanecer DRY, já que, mais uma vez, não podemos agrupar esses seletores específicos do navegador.

Os frames-chave

O objetivo é uma animação infinita que vai e volta. Os frames-chave de início e fim serão definidos no CSS. Só é necessário um frame-chave, o do meio em 50%, para criar uma animação que retorna ao ponto de partida repetidamente.

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

Segmentação de cada navegador

Nem todos os navegadores permitem a criação de pseudoelementos no próprio elemento <progress> ou a animação da barra de progresso. Mais navegadores oferecem suporte à animação da faixa do que a um pseudoelemento. Por isso, faço upgrade dos pseudoelementos como base para barras de animação.

Pseudoelemento do Chromium

O Chromium permite o pseudoelemento: ::after usado com uma posição para cobrir o elemento. As propriedades personalizadas indeterminadas são usadas, e a animação de ida e volta funciona muito bem.

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Barra de progresso do Safari

No Safari, as propriedades personalizadas e uma animação são aplicadas à barra de progresso do pseudoelemento:

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Barra de progresso do Firefox

No Firefox, as propriedades personalizadas e uma animação também são aplicadas à barra de progresso do pseudoelemento:

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

O JavaScript desempenha um papel importante com o elemento <progress>. Ele controla o valor enviado ao elemento e garante que haja informações suficientes no documento para os leitores de tela.

const state = {
  val: null
}

A demonstração oferece botões para controlar o progresso. Eles atualizam state.val e chamam uma função para atualizar o DOM.

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

É nessa função que ocorre a orquestração da interface/UX. Para começar, crie uma função setProgress(). Nenhum parâmetro é necessário porque ele tem acesso ao objeto state, ao elemento de progresso e à zona <main>.

const setProgress = () => {
  
}

Definindo o status de carregamento na zona <main>

Dependendo se o progresso está concluído ou não, o elemento <main> relacionado precisa de uma atualização no atributo aria-busy:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

Limpar atributos se o valor de carregamento for desconhecido

Se o valor for desconhecido ou não estiver definido, null neste uso, remova os atributos value e aria-valuenow. Isso vai mudar o <progress> para indeterminado.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

Corrigir problemas de matemática decimal do JavaScript

Como escolhi manter o máximo padrão de progresso de 1, as funções de incremento e decremento da demonstração usam matemática decimal. O JavaScript e outras linguagens nem sempre são bons nisso. Veja uma função roundDecimals() que vai cortar o excesso do resultado matemático:

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

Arredonde o valor para que ele possa ser apresentado e seja legível:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

Definir valor para leitores de tela e estado do navegador

O valor é usado em três locais no DOM:

  1. O atributo value do elemento <progress>.
  2. O atributo aria-valuenow.
  3. O conteúdo de texto interno de <progress>.
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

Foco no progresso

Com os valores atualizados, os usuários com visão vão ver a mudança no progresso, mas os usuários de leitores de tela ainda não vão receber o aviso de mudança. Foque no elemento <progress>, e o navegador vai anunciar a atualização.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

Captura de tela do app VoiceOver do Mac OS 
  lendo o progresso da barra de carregamento para o usuário.

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

Com certeza, eu faria algumas mudanças se tivesse outra chance. Acho que é possível limpar o componente atual e tentar criar um sem as limitações de estilo de pseudoclasse do elemento <progress>. Vale a pena conferir!

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web.

Crie uma demonstração, envie os links para mim no Twitter e eu vou adicioná-la à seção de remixes da comunidade abaixo.

Remixes da comunidade