Criar uma barra de progresso estilizada

O elemento <progress> é a maneira semântica de representar o progresso de conclusão de uma tarefa, como baixar ou enviar informações ou concluir parte de um processo de várias etapas. O uso do elemento nativo ajuda na acessibilidade, já que os leitores de tela têm um role="progressbar" implícito e podem anunciar valores.

Este guia implementa uma barra de progresso horizontal totalmente estilizada com personalizações de estilo.

Barra ou anel de progresso?

  • Se o espaço for limitado ou uma proporção quadrada for preferível, use uma barra de progresso circular (anel de progresso): anel de progresso.
  • Prefira ícones de carregamento para a maioria dos cenários de progresso indeterminado (ícone de carregamento). Use barras de progresso indeterminadas quando o estado indeterminado for temporário e for substituído em breve por uma barra de progresso determinada (por exemplo, aguardando metadados de um upload antes que uma porcentagem de progresso possa ser mostrada).

Marcação

  1. Use um elemento <progress> nativo: ele oferece acessibilidade integrada e comportamento consistente com a plataforma.
  2. Use max se for necessária uma escala diferente da padrão de 0 a 1.
  3. Para um valor de progresso específico, defina um atributo value. Omita value para um estado indeterminado (por exemplo, antes que um feedback mais concreto comece).
  4. Use <label for="...">, aria-labelledby ou aria-label para associar o elemento de progresso a um rótulo.

Que tipo de personalização é necessária?

Para uma estilização limitada, estas propriedades são suficientes:

  • accent-color para mudar a cor da barra de progresso
  • width / inline-size para mudar a largura da barra de progresso
  • height/block-size se aplica de forma inconsistente — teste primeiro
  • writing-mode: vertical-lr para barras verticais.
  • Efeitos que se aplicam após a pintura: opacity, filter, clip-path, máscaras, transformações, box-shadow etc.

Se a estilização limitada for suficiente, pule a próxima seção.

AVISO: algumas propriedades (por exemplo, border-radius, background, border) desativam silenciosamente o estilo padrão da barra de progresso. Se você precisar de um estilo personalizado além das propriedades acima, implemente um estilo totalmente personalizado, conforme descrito na próxima seção. NÃO aplique uma estilização mais extensa "só para ver se funciona".

Estilo totalmente personalizado

1. Configuração básica

  • OBRIGATÓRIO: use appearance: none para ativar o estilo personalizado.
  • Redefinir as bordas do UA com border: 0 solid
  • Para cortar a barra ativa no raio da borda do elemento, use overflow: clip OU aplique o mesmo border-radius à barra. NÃO use border-radius: inherit na barra, que depende da estrutura específica de elementos no DOM paralelo.
progress {
  /* Reset default browser styles */
  appearance: none;
  border: 0 solid;
  /* Clip bar to element’s border-radius  */
  overflow: clip;
}

2. Estilo

  • Estilize a faixa no próprio elemento progress.
  • Use o pseudoelemento apropriado para o navegador para segmentar a barra de progresso: ::-webkit-progress-value para WebKit/Blink e ::-moz-progress-bar para Firefox.
  • OBRIGATÓRIO: NÃO agrupe os pseudoelementos -webkit- e -moz- na mesma regra. A regra será considerada inválida e será ignorada. :is() e :where() não ajudam aqui, porque não são compatíveis com pseudoelementos.
  • Cada pseudoelemento segmentado por navegador precisa aparecer exatamente uma vez. As personalizações específicas de estado (como o tamanho do segmento indeterminado e a animação) precisam ser roteadas por propriedades personalizadas definidas no próprio elemento progress.
  • Opcional: faça a transição de inline-size no preenchimento para que as mudanças de valor determinadas sejam animadas em vez de pular. O Firefox dimensiona o preenchimento no layout e o ignora, o que é inofensivo.
progress {
  --progress-bar-radius: 1e5px;
  border-radius: var(--progress-bar-radius);
  /* Apply color for custom progress track (background) */
  background: var(--progress-bar-track, var(--color-neutral-90));
  /* Subtle depth on the empty part of the track */
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
}

/* Let <progress> styling show through */
progress::-webkit-progress-bar {
  background: transparent;
}

/* Webkit, Chrome */
progress::-webkit-progress-value {
  background: var(--progress-bar-fill, var(--accent-color));
  border-radius: var(--progress-bar-radius);
  transition: inline-size 0.3s;
}

/* Firefox */
progress::-moz-progress-bar {
  background: var(--progress-bar-fill, var(--accent-color));
  border-radius: var(--progress-bar-radius);
  transition: inline-size 0.3s;
}

3. Estilo de estado indeterminado

Adicione uma animação personalizada para o estado indeterminado: transforme o pseudoelemento de preenchimento em um segmento curto e deslize-o ao longo da faixa.

  • Estilize o preenchimento em si. NÃO o recrie como um plano de fundo em progress.
  • O preenchimento indeterminado existe apenas na animação: os frames-chave definem o comprimento com um inline-size real (isso supera o inline-size inline do UA do Chromium) e o movem com translate. NÃO use scale para o comprimento, porque isso distorce qualquer padrão pintado no preenchimento.
  • As porcentagens de translate são resolvidas em relação à largura do preenchimento. Portanto, expresse as posições em unidades de faixa: transforme progress em um contêiner de tamanho inline e use cqi nos frames-chave. Inclua margin-right: auto em from, to para que o Chromium também faça o preenchimento à esquerda física em RTL.
  • Use transformações, não margens, para mover o preenchimento.
    • As transformações mantêm a animação no compositor (a menos que inline-size também esteja sendo animado)
    • As margens são interrompidas em RTL por dois motivos:
    • O Firefox posiciona o preenchimento indeterminado usando apenas a margem esquerda física. Portanto, margin-inline-start não tem efeito em RTL, e o preenchimento nunca se move.
    • No Chromium e no WebKit, que definem o preenchimento como um bloco normal, quando uma margem empurra o preenchimento além do final da faixa, as margens somam mais do que a faixa. O CSS padrão resolve isso ignorando margin-left em RTL, de modo que o segmento salta para a borda em vez de deslizar para fora dela.
    • translate é aplicado após o layout, então nenhum dos problemas se aplica.
  • Para espelhar o movimento em RTL (como a barra nativa do Chrome faz), inverta a animação em :dir(rtl):
progress:dir(rtl) {
  --progress-bar-indeterminate-animation: progress-bar-indeterminate var(--progress-bar-indeterminate-animation-duration) infinite linear reverse;
}
  • Defina as propriedades públicas --progress-bar-indeterminate-* em progress (o nome delas já as limita ao estado) para que os consumidores possam substituí-las sem saber sobre :indeterminate.
  • Para variar o comprimento do segmento (por exemplo, a borda dianteira avança e a borda traseira se aproxima), defina --progress-bar-indeterminate-animation como duas animações simultâneas: uma em que os frames-chave animam translate em cqi (posição) e outra em que os frames-chave animam inline-size (comprimento), para que cada uma tenha os próprios frames-chave e easing. NÃO tente moldar as duas bordas em uma única lista de frames-chave. O animation-timing-function por frame-chave se aplica a todas as propriedades desse frame-chave.
  • --progress-bar-animation se aplica aos dois estados e pode ser usado para efeitos como um brilho determinado. revert-rule é usado para substituir apenas quando está definido, para maximizar a capacidade de composição.
  • Nunca desative a animação indeterminada (animation: none deixa um preenchimento de largura zero no Chromium). Para reduzir o movimento, diminua a velocidade.
progress {
  /* Lets the fill position itself in track units (cqi) */
  container-type: inline-size;
  /* Segment length relative to the track */
  --progress-bar-indeterminate-size: 30%;
  --progress-bar-indeterminate-animation-duration: 1.5s;

  /* Respect prefers-reduced-motion */
  @media (prefers-reduced-motion: reduce) {
    --progress-bar-indeterminate-animation-duration: 10s;
  }
}

progress:indeterminate {
  --progress-bar-animation: var(
      --progress-bar-indeterminate-animation,
      progress-bar-indeterminate var(--progress-bar-indeterminate-animation-duration) infinite linear
    );
}

/* Add to the existing pseudo-element rules */
progress::-webkit-progress-value {
  animation: var(--progress-bar-animation, revert-rule);
}

progress::-moz-progress-bar {
  animation: var(--progress-bar-animation, revert-rule);
}

/* Physical: left to right regardless of direction (reverse under :dir(rtl) to mirror). */
@keyframes progress-bar-indeterminate {
  from, to {
    inline-size: var(--progress-bar-indeterminate-size);
    /* Pin to the physical left; Chromium would otherwise lay the fill out at the inline start */
    margin-right: auto;
  }
  from { translate: -100%; } /* Own width: fully outside the left edge */
  to { translate: 100cqi; }  /* Track width: fully outside the right edge */
}

Outras considerações sobre acessibilidade

Além das diretrizes mencionadas acima:

  • Anunciar a conclusão sem roubar o foco: não defina tabindex nem chame .focus() em <progress>, porque ele não é interativo e mover o foco para ele atrapalha os usuários do teclado. Para anunciar quando uma tarefa em segundo plano é concluída, atualize um elemento de status educado (role="status" ou aria-live="polite").
  • Não adicione texto substituto dentro de <progress>: o texto dentro de <progress> é ignorado pelos navegadores mais recentes e não é usado por tecnologias adaptativas.
  • Evite aria-busy + aria-describedby em um <div> genérico sem rótulo. O aria-busy tem suporte inconsistente ao leitor de tela, e o aria-describedby em um <div> sem nome e sem função é ignorado pelas tecnologias adaptativas. Se o conteúdo de uma região não estiver pronto durante o carregamento, oculte-o com hidden ou aria-hidden="true".
  • RECOMENDADO: garanta um contraste adequado entre a barra de progresso e a faixa ao adicionar estilos personalizados.
  • Considere alternativas para representar o estado indeterminado: as animações de barra de progresso indeterminadas exibidas em paralelo com outro conteúdo podem ser consideradas uma distração para alguns usuários.

Estratégias de fallback

Alternativas e suporte do navegador para accent-color

A propriedade accent-color é um aprimoramento progressivo. Os navegadores que não são compatíveis com essa propriedade vão ignorá-la e usar as cores padrão da interface.

Se o accent-color não for compatível e for necessário estilizar a barra de progresso, use o método de estilo personalizado para aplicar o estilo que preferir.

Alternativas e suporte do navegador para revert-rule

Se não houver suporte para revert-rule, animation: var(--progress-bar-animation, revert-rule) vai voltar para animation-name: revert-rule com uma duração de 0s quando --progress-bar-animation não estiver definido, o que é uma operação inofensiva quando não há @keyframes revert-rule.