Como criar um componente da barra de carregamento

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

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

Demonstração clara e escura, indeterminada, crescente e conclusão no Chrome.

Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

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

Este desafio da GUI trabalhou com o elemento HTML <progress> para economizar algum esforço de acessibilidade. As cores e os layouts ampliam os limites da 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, fornecendo uma 
    visão geral do ícone adaptável de cima para baixo: 
    Safari, Firefox, Chrome.
Demonstração exibida no Firefox, Safari, iOS Safari, Chrome e Android Chrome em esquemas claros e escuros.

Marcação

Decidi envolver o elemento <progress> em um <label> para ignorar 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 leitura de tela possam retransmitir essas informações para o usuário.

<progress></progress>

Se não houver value, o progresso do elemento será indeterminado. O padrão do atributo max é 1, portanto, o progresso está entre 0 e 1. Definir max como 100, por exemplo, definiria o intervalo como 0 a 100. Optei por ficar dentro dos limites de 0 e 1, tratando valores de progresso para 0,5 ou 50%.

Progresso com wrapper de rótulo

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

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

Na demonstração, optei por incluir o rótulo apenas para leitores de tela. Para isso, envolva o texto do rótulo em uma <span> e aplique alguns estilos a ela para que fique efetivamente fora da tela:

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

Com o seguinte CSS complementar do WebAIM:

.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 do DevTools revelando o elemento somente pronto para a tela.

Área afetada pelo progresso de 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. Melhore isso atribuindo o atributo aria-busy ao elemento superior 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, deixei 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 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 do JavaScript. Isso é importante para a tecnologia do leitor de tela, já que dar foco ao progresso à medida que o progresso muda anuncia ao usuário até onde o progresso atualizado chegou.

Estilos

O elemento de progresso é um pouco complicado quando se trata de estilo. 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 para serem definidas.

Layout

Os estilos de layout visam permitir alguma flexibilidade no tamanho e na posição do rótulo do elemento de progresso. É adicionado um estado de conclusão especial que pode ser uma indicação visual útil, mas não obrigatória.

Layout de <progress>

A largura do elemento de progresso não é alterada para que possa diminuir e aumentar com o espaço necessário no design. Os estilos integrados são removidos ao definir appearance e border como none. Isso é feito para que o elemento possa ser normalizado em vários navegadores, já que cada navegador 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 de número científico para expressar um número grande, de modo que border-radius seja sempre arredondado. É equivalente a 1000px. Eu gosto de usar essa opção porque meu objetivo é usar um valor grande o suficiente para que eu possa defini-lo e esquecê-lo (e é mais curto que 1000px). Também é fácil de aumentar ainda mais, se necessário: basta mudar o 3 para 4, e 1e4px é equivalente a 10000px.

overflow: hidden é usado e tem sido um estilo polêmico. Isso facilitou algumas coisas, como não precisar transmitir valores de border-radius para a faixa e rastrear elementos de preenchimento, mas também fez com que nenhum filho do progresso pudesse 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 valor máximo com o valor. Se eles corresponderem, o progresso estará concluído. Quando concluído, um pseudoelemento é gerado e anexado ao final do elemento "progress", oferecendo uma boa indicação visual adicional para a 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% mostrando uma marca de seleção no final.

Cor

O navegador tem as próprias cores para o elemento "progress" e se adapta ao claro e ao escuro com apenas uma propriedade CSS. Isso pode ser baseado em alguns seletores especiais específicos para cada navegador.

Estilos de navegador claro e escuro

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

progress {
  color-scheme: light dark;
}

Cor preenchida do progresso de uma propriedade

Para a tonalidade de um elemento <progress>, use accent-color.

progress {
  accent-color: rebeccapurple;
}

A cor do plano de fundo da faixa muda de claro para escuro, dependendo da accent-color. O navegador está garantindo o contraste adequado: muito limpo.

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 dela. Na consulta de mídia prefers-color-scheme, forneça novos valores de cor para a faixa e acompanhe o progresso.

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, fornecemos ao elemento um índice de guias negativas para que ele pudesse ser focado de forma programática. Use :focus-visible para personalizar o foco e ativar o estilo de círculo de foco mais inteligente. Com isso, um clique do mouse e foco não vai mostrar o anel de foco, mas os cliques do teclado. O vídeo do YouTube aborda esse assunto em mais detalhes e vale a pena revisar.

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

Captura de tela da barra de carregamento com um anel de foco ao redor. Todas as cores são correspondentes.

Estilos personalizados em vários navegadores

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

  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 título Elementos, encontre e ative a caixa de seleção Mostrar shadow DOM do user agent.

Captura de tela do local no DevTools para ativar a exposição do shadow DOM do user agent.

Estilos do Safari e do Chromium

Navegadores baseados em WebKit, como Safari e Chromium, expõem ::-webkit-progress-bar e ::-webkit-progress-value, que permitem que um subconjunto de CSS seja usado. Por enquanto, defina background-color usando as propriedades personalizadas criadas anteriormente, que se adaptam ao claro e ao 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 &quot;progress&quot;.

Captura de tela do espaço de depuração, onde a barra de carregamento aparece em funcionamento no Safari, iOS Safari, 
  Firefox, Chrome e Chrome no Android.

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 pseudosseletores integrados no navegador, geralmente há um conjunto limitado de propriedades CSS permitidas.

Animar o preenchimento da faixa

Adicionar uma transição ao inline-size do elemento de progresso funciona no Chromium, mas não no 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 criar uma animação. Um pseudoelemento para o Chromium é criado e um gradiente é aplicado, animado de volta e para os 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 de CSS com aparência mágica. Veja a seguir um linear-gradient bastante complexo, mas com um bom nome. Sua finalidade e casos de uso podem ser claramente compreendidos.

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 ajudarão o código a permanecer DRY, já que não podemos agrupar esses seletores específicos do navegador.

Os frames-chave

O objetivo é uma animação infinita que vai para frente e para trás. Os frames-chave inicial e final serão definidos no CSS. Somente um frame-chave é necessário, o do meio em 50%, para criar uma animação que retorne ao ponto de onde ela começou várias vezes.

@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 a animar a faixa do que um pseudoelemento, então eu faço upgrade de pseudoelementos como base para barras de animação.

Pseudoelemento do Chromium

O Chromium permite o pseudoelemento ::after, que é usado com uma posição para cobrir o elemento. As propriedades personalizadas indeterminadas são usadas, e a animação de retorno 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 dos pseudoelementos:

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 dos pseudoelementos:

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 tem um papel importante com o elemento <progress>. Ele controla o valor enviado ao elemento e garante que haja informações suficientes no documento para leitores de tela.

const state = {
  val: null
}

A demonstração oferece botões para controlar o progresso. Eles atualizam state.val e, em seguida, 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 UI/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 = () => {
  
}

Como definir o status de carregamento na zona <main>

Dependendo se o progresso foi concluído ou não, o elemento <main> relacionado precisa de uma atualização para o 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 definido, null neste uso, remova os atributos value e aria-valuenow. Isso fará com que a <progress> se torne indeterminada.

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 matemáticos decimais do JavaScript

Como optei por manter o máximo padrão de progresso de 1, as funções de incremento e decremento de demonstração usam a matemática decimal. JavaScript e outras linguagens nem sempre são boas para isso. Confira 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 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 o valor dos leitores de tela e do 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 <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
}

Dar foco ao progresso

Com os valores atualizados, os usuários com visão normal vão notar a mudança do progresso, mas os usuários de leitores de tela ainda não vão receber o anúncio da mudança. Concentre-se no elemento <progress> e o navegador 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 Voice Over do Mac OS 
  lendo o progresso da barra de carregamento para o usuário.

Conclusão

Agora que você sabe como eu fiz isso, o que você faria‽ 🙂

Há algumas mudanças que eu gostaria de fazer se tivesse outra chance. Acho que há espaço para limpar o componente atual e para tentar criar um sem as limitações de estilo da pseudoclasse do elemento <progress>. Vale a pena explorar!

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

Crie uma demonstração, envie um tweet para mim (link em inglês) e eu vou adicionar o conteúdo à seção de remixes da comunidade abaixo.

Remixes da comunidade