Como criar um componente da barra de carregamento

Visão geral básica de como criar uma barra de carregamento adaptável e por cor com o elemento <progress>.

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

Demonstrações de cores claras e escuras, indeterminado, crescente e de conclusão no Chrome.

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

Visão geral

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

Este desafio da GUI trabalhou com o elemento HTML <progress> existente para economizar o 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 que fornecem uma
    visão geral do ícone adaptativo de cima para baixo: 
    Safari, Firefox, Chrome.
Demonstração mostrada no Firefox, Safari, iOS Safari, Chrome e Android Chrome em esquemas claros e escuros.

Marcação

Escolhi unir o elemento <progress> em um <label> para pular os atributos de relação explícitos em favor de um relacionamento implícito. Também identifiquei um elemento pai afetado pelo estado de carregamento para que as tecnologias de leitor de tela possam redirecionar essa informação ao 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-100. Escolhi permanecer dentro dos limites 0 e 1, traduzindo os valores de progresso para 0,5 ou 50%.

Progresso do 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 minha demonstração, escolhi incluir o rótulo somente para leitores de tela. Para fazer isso, una o texto da etiqueta a uma <span> e aplique alguns estilos a ela para que ela 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 &quot;Somente tela pronta&quot;.

Área afetada pelo progresso do carregamento

Para quem tem uma visão saudável, pode ser fácil associar um indicador de progresso a elementos e áreas de página relacionados. No entanto, isso não é tão claro para usuários com deficiência visual. Para melhorar isso, atribua o atributo aria-busy ao elemento na parte superior da tela 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, ele foi explícito para navegadores que não têm esse papel implícito. Também adicionei o atributo indeterminate para colocar o elemento explicitamente 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 "progress" focalizável do JavaScript. Isso é importante para a tecnologia do leitor de tela, já que atribuir o foco do progresso à medida que 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 estilo. Os elementos HTML integrados têm partes escondidas especiais que podem ser difíceis de selecionar e geralmente oferecem apenas um conjunto limitado de propriedades a serem definidas.

Layout

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

Layout de <progress>

A largura do elemento de progresso não é alterada para que ele possa encolher e aumentar com o espaço necessário no design. Os estilos integrados são removidos configurando appearance e border como none. Isso é feito para que o elemento possa ser normalizado em todos os navegadores, já que cada navegador tem estilos próprios 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. Gosto de usar isso porque meu objetivo é usar um valor grande o suficiente para que possa defini-lo e esquecê-lo (e é mais curto para gravar do que 1000px). Também é fácil aumentar ainda mais, se necessário: basta mudar de 3 para 4, e 1e4px é equivalente a 10000px.

overflow: hidden é usado e tem sido um estilo controverso. Isso facilitou algumas coisas, como não precisar transmitir valores border-radius para a faixa e rastrear elementos de preenchimento. Mas 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 e, se corresponderem, o progresso estará concluído. Quando concluído, um pseudoelemento é gerado e anexado ao final do elemento "progress", oferecendo um bom indicativo visual 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% com uma marca de seleção no final.

Cor

O navegador traz as próprias cores para o elemento de progresso e é adaptável a claro e escuro com apenas uma propriedade CSS. Isso pode ser baseado em alguns seletores especiais específicos do navegador.

Estilos de navegador claros e escuros

Para usar um elemento <progress> adaptável claro e escuro no seu site, o color-scheme é suficiente.

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;
}

A cor de fundo da faixa muda de clara para escura, dependendo do accent-color. O navegador está garantindo o contraste adequado: ótimo.

Cores claras e escuras totalmente personalizadas

Defina duas propriedades personalizadas no elemento <progress>: uma para a cor da faixa e outra para a cor de progresso dela. Na consulta de mídia prefers-color-scheme, forneça novos valores de cor para 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 maneira programática. Use :focus-visible para personalizar o foco e ativar o estilo de anel de foco mais inteligente. Com isso, um clique do mouse e um foco não mostram o anel de foco, mas os cliques do teclado sim. O vídeo do YouTube explica isso em mais detalhes e vale a pena analisar.

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

Captura de tela da barra de carregamento com um anel de foco em volta. 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 uso do elemento "progress" é uma tag única, mas é composta de alguns elementos filhos que são expostos por pseudosseletores de CSS. O Chrome DevTools mostrará estes elementos se você ativar a configuração:

  1. Clique com o botão direito do mouse na sua página e selecione Inspecionar elemento para abrir o DevTools.
  2. Clique na engrenagem Configurações no canto superior direito da janela DevTools.
  3. No título Elementos, localize e ative a caixa de seleção Mostrar DOM da sombra do user agent.

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

Estilos Safari e 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 à luz 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 &quot;progress&quot;.

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 canto de depuração em que o Safari, o iOS Safari,
  o Firefox, o Chrome e o Chrome no Android têm a barra de carregamento funcionando.

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

Animação

Ao trabalhar com pseudosseletores integrados ao navegador, muitas vezes há um conjunto limitado de propriedades CSS permitidas.

Animar a faixa preenchendo

Adicionar uma transição para a 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 em ::-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 é um pouco mais criativo para poder fornecer uma animação. Um pseudoelemento é criado para o Chromium e um gradiente é aplicado, animado para os três navegadores.

Propriedades personalizadas

Propriedades personalizadas são ótimas para muitas coisas, mas uma das minhas favoritas é simplesmente dar um nome a um valor CSS de aparência mágica. Confira abaixo um linear-gradient bastante complexo, mas com um nome legal. Sua finalidade e casos de uso podem ser claramente entendidos.

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, porque não é possível agrupar esses seletores específicos do navegador.

Os frames-chave

A meta é uma animação infinita que vai e volta. Os frames-chave inicial e final serão definidos no CSS. Apenas um frame-chave é necessário, o do meio em 50%, para criar uma animação que retorne ao ponto de onde começou, repetidas vezes.

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

Segmentação por navegador

Nem todos os navegadores permitem a criação de pseudoelementos no próprio elemento <progress> ou permitem animar a barra de progresso. Mais navegadores oferecem suporte à animação da faixa do que um pseudoelemento, então faço upgrade de 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 retorno e retorno 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 desempenha um papel importante com o elemento <progress>. Ele controla o valor enviado ao elemento e garante que informações suficientes estejam presentes 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 depois 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 = () => {
  
}

Como definir o status de carregamento na zona <main>

Dependendo de o progresso ser concluído ou não, o elemento <main> relacionado precisa ser atualizado para o atributo aria-busy:

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

Limpar atributos se a quantidade de carregamento for desconhecida

Se o valor for desconhecido ou não for definido, null nesse uso, remova os atributos value e aria-valuenow. Isso vai tornar a <progress> 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 em JavaScript

Como escolhi manter o máximo padrão de progresso de 1, as funções de incremento e decremento de demonstração usam matemática decimal. JavaScript e outras linguagens nem sempre são boas nisso. Confira uma função roundDecimals() que 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 ficar 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 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
}

Focar no progresso

Com os valores atualizados, os usuários com deficiência visual vão notar a mudança do progresso, mas os leitores de tela ainda não serão avisados. Concentre-se no elemento <progress> para que o navegador anuncie 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 de 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 e os adicionarei à seção de remixes da comunidade abaixo.

Remixes da comunidade