Uma visão geral fundamental de como criar uma barra de carregamento adaptável a cores e acessível com o elemento <progress>
.
Nesta postagem, quero compartilhar ideias sobre como criar uma barra de carregamento adaptável a cores e acessível com o elemento <progress>
. Teste a
demonstração e veja a
fonte.
Se preferir vídeo, confira uma versão desta postagem no YouTube:
Visão geral
O elemento
<progress>
oferece feedback visual e audível aos usuários sobre a conclusão. Esse feedback visual é valioso para cenários como: progresso em um formulário, exibição de informações de download ou upload ou até mesmo mostrar que a quantidade de progresso é desconhecida, mas o trabalho ainda está ativo.
Esse desafio de GUI funcionou com o elemento HTML <progress>
atual para economizar algum esforço em 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.

Marcação
Optei por incluir 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 leitura de tela possam transmitir essas informações de volta ao usuário.
<progress></progress>
Se não houver value
, o progresso do elemento será
indeterminado.
O atributo max
tem como padrão o valor 1, então o progresso fica entre 0 e 1. Definir max
como 100, por exemplo, define o intervalo como 0 a 100. Escolhi ficar dentro dos limites 0 e 1, traduzindo valores de progresso para 0,5 ou 50%.
Progresso com rótulo
Em um relacionamento implícito, um elemento de progresso é agrupado por um rótulo assim:
<label>Loading progress<progress></progress></label>
Na minha demonstração, escolhi incluir o rótulo apenas para leitores de tela.
Isso é feito envolvendo o texto do rótulo em um <span>
e aplicando alguns estilos
para que ele fique efetivamente fora da tela:
<label>
<span class="sr-only">Loading progress</span>
<progress></progress>
</label>
Com o seguinte CSS complementar da 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;
}
Área afetada pelo progresso do carregamento
Se você tem uma visão saudável, é fácil associar um indicador de progresso
a elementos e áreas da 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 ela for concluída.
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 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 em JavaScript. Isso é importante para
a tecnologia de leitor de tela, já que dar 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 estilização. Os elementos HTML integrados têm partes ocultas 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 permitem alguma 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 ú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 seus 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 notação de número científico para expressar um número grande. Assim, o border-radius
é 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 de escrever do que 1000px
). Também é fácil aumentar ainda mais, se necessário: basta mudar o 3 para um 4. Assim, 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 preencher elementos da faixa. No entanto, isso também significava que nenhum filho do progresso
podia ficar fora do elemento. Outra iteração nesse elemento de progresso personalizado pode ser feita sem overflow: hidden
, o que pode abrir algumas oportunidades para animações ou estados de conclusão melhores.
Progresso concluído
Os seletores de CSS fazem o trabalho pesado aqui comparando o máximo com o valor. Se eles corresponderem, o progresso será concluído. Quando concluído, um pseudo-elemento é gerado e anexado ao final do elemento de progresso, fornecendo uma dica visual adicional interessante 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);
}
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 feito com alguns seletores especiais específicos do navegador.
Estilos de navegador claro e escuro
Para ativar um elemento <progress>
adaptável a temas escuros e claros no seu site,
basta usar color-scheme
.
progress {
color-scheme: light dark;
}
Cor preenchida do progresso de uma única propriedade
Para colorir um elemento <progress>
, use accent-color
.
progress {
accent-color: rebeccapurple;
}
Observe que a cor de fundo da faixa muda de clara para escura dependendo do
accent-color
. O navegador garante o contraste adequado: muito 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 dela.
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
Antes, atribuímos ao elemento um índice de tabulação negativo para que ele pudesse ser focado
programaticamente. Use
:focus-visible
para
personalizar o foco e ativar o estilo de anel de foco mais inteligente. Assim, um clique do mouse e o foco não vão mostrar o anel de foco, mas os cliques do teclado vão. O vídeo do YouTube explica isso com mais detalhes e vale a pena assistir.
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}
Estilos personalizados em navegadores
Personalize os estilos selecionando as partes de um elemento <progress>
que cada navegador expõe. O uso do elemento de progresso é uma única tag, mas é feito de alguns elementos filhos expostos por pseudoseletores CSS. O Chrome DevTools
vai mostrar esses elementos se você ativar a configuração:
- Clique com o botão direito do mouse na página e selecione Inspecionar elemento para abrir o DevTools.
- Clique na engrenagem de configurações no canto superior direito da janela do DevTools.
- No cabeçalho Elementos, encontre e marque a caixa de seleção Mostrar DOM de sombra do user agent.
Estilos do Safari e do Chromium
Navegadores baseados no WebKit, como Safari e 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 ao modo claro e escuro.
/* Safari/Chromium */
progress[value]::-webkit-progress-bar {
background-color: var(--_track);
}
progress[value]::-webkit-progress-value {
background-color: var(--_progress);
}
Estilos do Firefox
O Firefox expõe apenas 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);
}
Observe que o Firefox tem uma cor de faixa definida em accent-color
, enquanto o Safari do iOS tem uma faixa azul claro. 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.
Animação da faixa sendo preenchida
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 fico um pouco mais criativo para fornecer uma animação. Um pseudoelemento para o Chromium é criado, e um gradiente é aplicado, que é animado para frente e para trás em todos 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 CSS que parece mágico. A seguir, um linear-gradient
bastante
complexo,
mas com um nome legal. O objetivo e os 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 ajudam a manter o código 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 keyframes de início e fim serão definidos em CSS. Só é necessário um frame-chave, o do meio em 50%
, para criar uma animação que volta ao ponto de partida 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 são compatíveis com a animação da faixa do que um pseudoelemento. Por isso, 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 vai e vem 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 tem um papel importante no 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 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. Comece criando 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 = () => {
}
Definir 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 do carregamento for desconhecido
Se o valor for desconhecido ou não definido, use 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 em 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.
Confira uma função roundDecimals()
que vai remover 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:
- O atributo
value
do elemento<progress>
. - O atributo
aria-valuenow
. - O conteúdo do 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
}
Foco no progresso
Com os valores atualizados, os usuários com visão vão notar a mudança no progresso, mas os usuários de leitores de tela ainda não recebem o anúncio da mudança. Foque o 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()
}
Conclusão
Agora que você sabe como eu fiz, como você faria? 🙂
Com certeza, 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, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.