Círculo de progreso

Un anillo de progreso (o barra de progreso circular) proporciona comentarios visuales sobre el estado de una tarea. A diferencia de una barra de progreso lineal, su forma circular es ideal para paneles, componentes de tarjetas o cualquier lugar donde el espacio sea limitado.

En esta guía, se implementa un círculo de progreso de la siguiente manera:

  • Usa el elemento nativo <progress> como base semántica para garantizar que el componente sea accesible para los lectores de pantalla y los usuarios de teclado de forma predeterminada.
  • Se aplica estilo al componente con conic-gradient() y mask-image, lo que permite un anillo completamente responsivo y con temas sin la complejidad de la manipulación de rutas de SVG.
  • Aprovechar las propiedades personalizadas de CSS y @property para habilitar transiciones fluidas y aceleradas por GPU del relleno de progreso

Este enfoque es preferible a las soluciones que solo usan SVG, ya que utiliza el elemento semántico <progress> en lugar de ARIA y se integra con mayor facilidad en el diseño, los sistemas de diseño y la tipografía existentes.

Consulta el indicador giratorio de carga para controlar los estados de carga indeterminados.

Implementación

1. Marca

Usa un wrapper para contener el anillo visual y el contenido central opcional. El elemento <progress> sigue siendo la fuente de información semántica. Usa una clase de utilidad para ocultar visualmente la barra de progreso nativa y, al mismo tiempo, mantenerla accesible.

<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

Cómo ocultar la IU nativa

Para aplicar diseño al elemento <progress> como un círculo de progreso, primero oculta el diseño predeterminado del navegador para las barras de progreso.

/* 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;
}

Contenedor y anillo

El wrapper proporciona el contexto de posicionamiento. El elemento <progress> controla el 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;
}

También puedes usar un radial-gradient para crear extremos redondeados.

Habilita transiciones fluidas con @property

Para animar el círculo de progreso de forma fluida cuando cambia el valor, registra --value como una propiedad personalizada numérica.

Los usuarios con sensibilidad al movimiento pueden encontrar desorientadora la transición entre valores. Respeta la consulta de medios prefers-reduced-motion con una duración predeterminada de 0 segundos (inmediata) y establece un tiempo más largo para los usuarios que no tengan preferencias.

@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. Actualizaciones del progreso

Actualiza el atributo value en el elemento <progress> y el contenido de texto de .progress-ring-content (si se muestra un porcentaje) cada vez que cambie el valor.

4. Estado de éxito opcional

Puedes usar el selector de atributos de CSS para actualizar automáticamente la apariencia del anillo (p.ej., cambiar el color a verde) cuando se complete la tarea.

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

Estrategias de resguardo

No agregues un valor de resguardo dentro del elemento <progress>. La tecnología de accesibilidad no lo usa, y todos los navegadores modernos lo ignoran.

Resguardo de animación

Si no se admite @property, el anillo saltará al valor nuevo de forma instantánea en lugar de realizar una transición fluida. En la mayoría de los casos, esto es correcto y no interrumpe ninguna funcionalidad.

Si la transición es absolutamente necesaria, puedes verificar la compatibilidad con @property y usar un bucle requestAnimationFrame() para interpolar --value en navegadores más antiguos.

En el caso de los navegadores que aún no admiten background-clip: border-area, recurre a un mask-image para vaciar el centro del 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;
}

En el caso de los navegadores que no admiten la función de CSS attr() para ninguna propiedad, usa un MutationObserver para sincronizar automáticamente el atributo value con la propiedad 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"],
  });
}