Anel de progresso

Um anel de progresso (ou barra de progresso circular) fornece feedback visual sobre o status de uma tarefa. Ao contrário de uma barra de progresso linear, o formato circular é ideal para painéis, componentes de cartão ou qualquer lugar com espaço limitado.

Este guia implementa um anel de progresso:

  • Usar o elemento nativo <progress> como base semântica para garantir que o componente seja acessível a leitores de tela e usuários de teclado sem precisar de configurações adicionais.
  • Estilizar o componente com conic-gradient() e mask-image, permitindo um anel totalmente responsivo e com tema, sem a complexidade da manipulação de caminhos SVG.
  • Uso de propriedades personalizadas de CSS e @property para permitir transições suaves e aceleradas por GPU do preenchimento de progresso.

Essa abordagem é preferível em relação às soluções somente SVG porque usa o elemento semântico <progress> em vez de ARIA e se integra mais facilmente ao layout, aos sistemas de design e à tipografia atuais.

Consulte o ícone de carregamento para lidar com estados de carregamento indeterminados.

Implementação

1. Marcação

Use um wrapper para armazenar o anel visual e o conteúdo central opcional. O elemento <progress> continua sendo a fonte semântica da verdade. Use uma classe de utilitário para ocultar visualmente a barra de progresso nativa, mantendo-a acessível.

<div class="ring-wrapper">
  <progress value="75" max="100" aria-label="Task progress" class="progress-ring"></progress>
  <!-- Optional: Content to display in the center -->
  <div class="ring-content">
    75%
  </div>
</div>

2. Estilos

Como ocultar a interface nativa

Para estilizar o elemento <progress> como um anel de progresso, primeiro oculte o estilo padrão do navegador para barras de progresso.

/* Hide native bars */
progress.loading-spinner:indeterminate::-webkit-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-webkit-progress-value {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-moz-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::slider-fill {
  display: none;
  background: none;
}

Contêiner e anel

O wrapper fornece o contexto de posicionamento. O elemento <progress> processa o gradiente visual.

.progress-ring-wrapper {
  position: relative;
  display: grid;
  place-items: center;
}

progress.progress-ring {
  --size: 150px;
  --thickness: 16px;
  --track-color: #f1f5f9;
  --fill-color: #3b82f6;
  --value: attr(value type(<number>));
  width: var(--size);
  height: var(--size);

  transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 50%;

  background: conic-gradient(
    var(--fill-color) calc(var(--value) * 1%),
    var(--track-color) 0
  );

  /* MANDATORY: Clip the background to the border-area. */
  background-clip: border-area;
  border: var(--thickness) solid transparent;
  background-origin: border-box;
}

.progress-ring-content {
  /* Positioned in the center of the wrapper */
  position: absolute;
}

Também é possível usar um radial-gradient para criar extremidades arredondadas.

Ativar transições suaves com @property

Para animar o anel de progresso de forma suave quando o valor mudar, registre --value como uma propriedade personalizada numérica.

Usuários com sensibilidade ao movimento podem achar a transição entre valores desorientadora. Respeite a consulta de mídia prefers-reduced-motion com uma duração de 0 segundo (imediata) por padrão e defina um tempo maior para usuários sem preferência.

@property --value {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

progress.progress-ring {
  transition: --value 0s ease-in-out;
  @media (prefers-reduced-motion: no-preference) {
    transition-duration: 0.4s;
  }
}

3. Atualizações de progresso

Atualize o atributo value no elemento <progress> e o conteúdo de texto de .progress-ring-content (se estiver mostrando uma porcentagem) sempre que o valor mudar.

4. Estado de sucesso opcional

Você pode usar o seletor de atributos CSS para atualizar automaticamente a aparência do anel (por exemplo, mudando a cor para verde) quando a tarefa for concluída.

/* Change the fill color to green when the progress reaches 100% */
progress.progress-ring[value="100"] {
  --fill-color: #10b981;
}

Estratégias de fallback

Não adicione um valor substituto dentro do elemento <progress>. Ele não é usado por tecnologias adaptativas e é ignorado por todos os navegadores mais recentes.

Animação substituta

Se @property não for compatível, o anel vai pular para o novo valor instantaneamente, em vez de fazer uma transição suave. Na maioria dos casos, isso não é um problema e não prejudica nenhuma funcionalidade.

Se a transição for absolutamente necessária, verifique se há suporte para @property e use um loop requestAnimationFrame() para interpolar --value em navegadores mais antigos.

Para navegadores que ainda não oferecem suporte a background-clip: border-area, use um mask-image para esvaziar o centro do elemento <progress>.

@supports not (background-clip: border-area) {
  mask-image: radial-gradient(
    transparent calc(50% - var(--thickness)),
    black calc(50% - var(--thickness) + 0.5px)
  );
  border: 0;
}

Para navegadores que não são compatíveis com a função CSS attr() para qualquer propriedade, use um MutationObserver para sincronizar automaticamente o atributo value com a propriedade personalizada --value.

if (!CSS.supports("width: attr(value type(<number>))")) {
  const observer = new MutationObserver(() => {
    progress.style.setProperty("--value", progress.getAttribute("value"));
  });
  observer.observe(progress, {
    attributes: true,
    attributeFilter: ["value"],
  });
}