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()emask-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
@propertypara 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"],
});
}