Uma visão geral básica de como criar uma barra de carregamento acessível e adaptável a cores com o elemento <progress>
.
Neste post, quero compartilhar ideias sobre como criar uma barra de carregamento adaptável e
acessível com o elemento <progress>
. Teste a
demonstração e confira o
código-fonte.
Se preferir vídeos, confira a versão desta postagem no YouTube:
Visão geral
O elemento
<progress>
oferece feedback visual e sonoro 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 mostrando que o
valor do progresso é desconhecido, mas o trabalho ainda está ativo.
Este desafio de GUI trabalhou com
o elemento HTML <progress>
para economizar um pouco de esforço na acessibilidade. As
cores e os layouts ultrapassam 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
Escolhi agrupar o elemento <progress>
em um
<label>
para
ignorar os atributos de relacionamento explícitos em favor de uma
relação implícita.
Também marquei 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 está entre 0 e 1. Por exemplo, definir max
como 100 definiria o intervalo como 0 a 100. Eu 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 é agrupado 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.
Isso é feito envolvendo o texto do rótulo em um <span>
e aplicando alguns estilos
a ele para que ele fique fora da tela:
<label>
<span class="sr-only">Loading progress</span>
<progress></progress>
</label>
Com o CSS a seguir 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;
}
Área afetada pelo progresso do carregamento
Se você tem visão normal, pode ser fácil associar um indicador de progresso
a elementos relacionados e áreas da página, mas para usuários com deficiência visual, isso não é
tão claro. Para melhorar isso, atribua o atributo
aria-busy
ao elemento principal 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, mude 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
, tornei-o 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 de progresso focado em JavaScript. Isso é importante para
a tecnologia de leitor de tela, já que dar o foco ao progresso conforme ele muda
informa ao usuário o quanto o progresso atualizado foi alcançado.
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 alguma flexibilidade no tamanho e na posição do rótulo do elemento de progresso. 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 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 deles 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 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 configurá-lo e esquecê-lo (e é mais curto para escrever do que 1000px
). Também é
fácil torná-lo ainda maior, se necessário: basta mudar o 3 para um 4, e 1e4px
será
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 elementos de preenchimento da faixa. No entanto, isso também significava que nenhum filho do progresso
poderia existir 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 melhores estados de conclusão.
Progresso concluído
Os seletores de CSS fazem o trabalho difícil aqui comparando o máximo com o valor. Se eles corresponderem, o progresso será concluído. Quando concluído, um pseudoelemento é gerado e anexado ao final do elemento de progresso, fornecendo uma boa dica 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);
}
Cor
O navegador traz as próprias cores para o elemento de progresso e se adapta ao modo claro e escuro com apenas uma propriedade CSS. Isso pode ser desenvolvido com alguns seletores especiais específicos do navegador.
Estilos claros e escuros do navegador
Para ativar um elemento <progress>
adaptável escuro e claro no seu site,
é necessário 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 de fundo da faixa muda de claro para escuro, dependendo do
accent-color
. O navegador está garantindo 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 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, atribuímos ao elemento um índice de guia negativo para que ele pudesse ser focado
programaticamente. Use
:focus-visible
para
personalizar o foco e ativar o estilo mais inteligente do anel de foco. Com isso, um clique e foco do mouse
não vai mostrar o anel de foco, mas os cliques do teclado vão. O
vídeo do YouTube aborda esse assunto em mais detalhes e
vale a pena assistir.
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}
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 de progresso é uma única tag, mas é composto por
alguns elementos filhos expostos por pseudosseletores do CSS. As Ferramentas do desenvolvedor do Chrome
vão 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 das Ferramentas do desenvolvedor.
- No título Elements, encontre e ative a caixa de seleção Show user agent shadow DOM.
Estilos do Safari e do Chromium
Navegadores baseados no WebKit, como o Safari e o 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 a temas claros e escuros.
/* 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 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);
}
O Firefox tem uma cor de trilha definida em accent-color
, enquanto o Safari para iOS
tem uma trilha 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 no Webkit.
Animação
Ao trabalhar com pseudoseletores integrados ao navegador, geralmente é com um conjunto limitado de propriedades CSS permitidas.
Como 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 sou um pouco mais criativo para poder fornecer uma animação. Um pseudoelemento para o Chromium é criado e um gradiente é aplicado com animação 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 com aparência mágica. A seguir, há uma linear-gradient
bastante complexa,
mas com um nome legal. A finalidade e os casos de uso podem ser entendidos claramente.
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 não é possível 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 de término
serão definidos no CSS. Apenas um frame-chave é necessário, o frame-chave do meio
em 50%
, para criar uma animação que retorna ao ponto de partida, sempre.
@keyframes progress-loading {
50% {
background-position: left;
}
}
Segmentar cada navegador
Nem todos os navegadores permitem a criação de pseudoelementos no 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, mudei de pseudoelementos como
base para barras de animação.
Pseudoelemento do Chromium
O Chromium permite que o pseudoelemento: ::after
seja 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 para o 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 chamam uma função para atualizar o
DOM.
document.querySelector('#complete').addEventListener('click', e => {
state.val = 1
setProgress()
})
setProgress()
Essa função é onde 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 = () => {
}
Como definir o status de carregamento na zona <main>
Dependendo se o progresso está completo 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 a quantidade de carregamento for desconhecida
Se o valor for desconhecido ou não definido, null
nesse uso, remova os atributos value
e
aria-valuenow
. Isso vai tornar o <progress>
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 outros
idiomas nem sempre são bons
nisso.
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 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
}
Como dar foco ao 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 receberam o aviso 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 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 links para mim e vou adicionar à seção de remixes da comunidade abaixo.