Ícono giratorio de carga

Un ícono giratorio de carga (o indicador de actividad) informa a los usuarios que hay un proceso en curso cuando se desconoce la duración exacta. A diferencia de un círculo de progreso, un indicador giratorio es "indeterminado" y, por lo general, usa una animación para indicar la actividad.

En esta guía, se implementa un ícono giratorio de la siguiente manera:

  • Usar el elemento <progress> nativo como base semántica Si omites el atributo value, el navegador lo trata como una barra de progreso indeterminada, lo que garantiza que las tecnologías de accesibilidad lo anuncien correctamente.
  • Darle estilo al componente con conic-gradient() para crear un "rastro" visual y con background-clip: border-area para ahuecar el centro y convertirlo en un anillo
  • Animar el ícono giratorio de forma eficiente con transformaciones de CSS y respetar prefers-reduced-motion para garantizar una experiencia cómoda para todos los usuarios

Consulta Anillo de progreso para controlar tareas determinadas con una duración conocida.

Implementación

1. Marca

Usa el elemento <progress> nativo como la fuente de información semántica y el componente visual. Sin un atributo value, es implícitamente indeterminado.

<progress aria-label="Loading" class="loading-spinner"></progress>

2. Estilos

Cómo ocultar la IU nativa

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

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

Anillo y estela del spinner

El ícono giratorio usa un conic-gradient para crear un rastro visual.

@property --arc-start {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@property --arc-end {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
/* Use a custom property to conditionally include the dash animation */
@property --progress-dash-animation {
  syntax: "*";
  inherits: false;
  initial-value: , progress-dash 3s ease-in-out infinite;
}

progress.loading-spinner:indeterminate {
  --_from: calc(90deg + var(--arc-start, 0deg));
  --_to: calc(90deg + var(--arc-end, 158deg));
  --size: 40px;
  --thickness: 2px;
  --spinner-color: #3b82f6;
  --track-color: #e2e5e7;
  --spinner-duration: 1.5s;
  --_used-spinner-duration: var(--spinner-duration);
  --spinner-timing: linear;

  position: relative;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  appearance: none;

  /* Create the fading trail with dynamic angles */
  background: conic-gradient(
      from var(--_from),
      var(--spinner-color) calc(var(--_to) - var(--_from)),
      transparent 0
    )
    var(--track-color);

  @supports (background-clip: border-area) {
    background-clip: border-area;
    border: var(--thickness) solid transparent;
    background-origin: border-box;
  }

  /* ... fallback for background-clip: border-area ... */

  /* The dash animation is only included if @property is supported */
  animation:
    progress-spin var(--_used-spinner-duration) linear infinite
    var(--progress-dash-animation, );
}

@keyframes progress-spin {
  to {
    rotate: 1turn;
  }
}

@keyframes progress-dash {
  from {
    --arc-start: 0deg;
    --arc-end: 3deg;
  }
  50% {
    --arc-start: 100deg;
    --arc-end: 358deg;
  }
  to {
    --arc-start: 360deg;
    --arc-end: 363deg;
  }
}

Cómo respetar las preferencias de movimiento

Los usuarios con sensibilidad al movimiento pueden encontrar desorientadores los elementos que giran rápidamente. Respeta siempre la consulta de medios prefers-reduced-motion. Establece la propiedad interna --_used-spinner-duration para anular el valor --spinner-duration del usuario.

@media (prefers-reduced-motion: reduce) {
  .loading-spinner {
    /* Slow down the animation significantly rather than stopping it entirely,
       so the user still knows that the process is active. */
    --_used-spinner-duration: 6s;
  }
}

Estrategias de resguardo

Si se admite @property, la animación de guiones se incluye automáticamente a través de initial-value de la propiedad --progress-dash-animation. En los navegadores que no admiten @property, se ignora el registro de la propiedad y la animación recurre a una rotación simple. No se requiere JavaScript para este resguardo.

En el caso de los navegadores que aún no admiten background-clip: border-area, recurre a un mask-image para vaciar el centro.

/* Fallback: use mask-image to create the ring */
@supports not (background-clip: border-area) {
  --clip-boundary: calc(100% - var(--thickness));
  mask-image: radial-gradient(
    farthest-side,
    transparent var(--clip-boundary),
    black var(--clip-boundary)
  );
  border: 0;
}