Cómo compilar una barra de progreso con diseño

El elemento <progress> es la forma semántica de representar el progreso de finalización de una tarea, como descargar o subir información, o completar parte de un proceso de varios pasos. El uso del elemento nativo ayuda con la accesibilidad, ya que los lectores de pantalla tienen un role="progressbar" implícito y pueden anunciar valores.

En esta guía, se implementa una barra de progreso horizontal con un diseño completo y personalizaciones de estilo.

¿Barra de progreso o círculo de progreso?

  • Si el espacio es limitado o se prefiere una relación de aspecto cuadrada, considera usar una barra de progreso circular (círculo de progreso): Círculo de progreso
  • Se prefieren los íconos giratorios para la mayoría de las situaciones de progreso indeterminado (Ícono giratorio de carga). Usa barras de progreso indeterminadas cuando el estado indeterminado sea temporal y pronto se reemplace por una barra de progreso determinada (p. ej., cuando se esperan metadatos sobre una carga antes de que se pueda mostrar un porcentaje de progreso).

Marca

  1. Usa un elemento <progress> nativo: Proporciona accesibilidad integrada y un comportamiento coherente con la plataforma.
  2. Usa max si se necesita una escala diferente de la predeterminada de 0 a 1.
  3. Para un valor de progreso específico, establece un atributo value. Omite value para un estado indeterminado (p.ej., antes de que comience un feedback más concreto).
  4. Usa <label for="...">, aria-labelledby o aria-label para asociar el elemento de progreso con una etiqueta.

¿Qué tipo de personalización se necesita?

Para aplicar un diseño limitado, estas propiedades son suficientes:

  • accent-color para cambiar el color de la barra de progreso
  • width/inline-size para cambiar el ancho de la barra de progreso
  • height/block-size se aplica de forma inconsistente; realiza una prueba primero
  • writing-mode: vertical-lr para las barras verticales.
  • Efectos que se aplican después de pintar: opacity, filter, clip-path, máscaras, transformaciones, box-shadow, etcétera.

Si el diseño limitado es suficiente, omite la siguiente sección.

ADVERTENCIA: Ciertas propiedades (p.ej., border-radius, background, border) inhabilitarán de forma silenciosa el diseño predeterminado de la barra de progreso. Si necesitas un diseño que vaya más allá de las propiedades anteriores, debes implementar un diseño completamente personalizado, como se describe en la siguiente sección. NO apliques un diseño más extenso "por si acaso funciona".

Estilo completamente personalizado

1. Configuración básica

  • OBLIGATORIO: Usa appearance: none para habilitar el diseño personalizado.
  • Restablece los límites de UA con border: 0 solid
  • Para recortar la barra activa según el radio del borde del elemento, usa overflow: clip O aplica el mismo border-radius a la barra. NO uses border-radius: inherit en la barra, ya que depende de la estructura específica de los elementos en el DOM de sombra.
progress {
  /* Reset default browser styles */
  appearance: none;
  border: 0 solid;
  /* Clip bar to element’s border-radius  */
  overflow: clip;
}

2. Diseño

  • Aplica diseño a la pista en el elemento progress.
  • Usa el seudoelemento adecuado para el navegador para orientar la barra de progreso: ::-webkit-progress-value para WebKit/Blink y ::-moz-progress-bar para Firefox.
  • OBLIGATORIO: NO agrupes los seudoelementos -webkit- y -moz- en la misma regla. La regla se considerará no válida y se ignorará. :is() y :where() no ayudan en este caso, ya que no admiten seudoelementos.
  • Cada seudoelemento orientado al navegador debe aparecer exactamente una vez. Las personalizaciones específicas del estado (como el tamaño del segmento indeterminado y la animación) se deben enrutar a través de las propiedades personalizadas establecidas en el propio elemento progress.
  • Opcional: Agrega una transición inline-size en el relleno para que los cambios de valor determinados se animen en lugar de saltar. Firefox ajusta el tamaño del relleno en el diseño y lo ignora, lo que no causa problemas.
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

Agrega una animación personalizada para el estado indeterminado: Convierte el pseudoelemento de relleno en un segmento corto y deslízalo por la pista.

  • Aplica diseño al relleno en sí; NO lo vuelvas a crear como fondo en progress.
  • El relleno indeterminado solo existe a través de su animación: los fotogramas clave establecen su longitud con un inline-size real (esto supera al inline-size intercalado del UA de Chromium) y lo mueven con translate. NO uses scale para la longitud, ya que distorsiona cualquier patrón pintado en el relleno.
  • Los porcentajes de translate se resuelven en función del ancho del relleno, por lo que debes expresar las posiciones en unidades de pista: convierte progress en un contenedor de tamaño intercalado y usa cqi en los fotogramas clave. Incluye margin-right: auto en from, to para que Chromium también coloque el relleno en la parte física izquierda en RTL.
  • Usa transformaciones, no márgenes, para mover el relleno.
    • Las transformaciones mantienen la animación en el compositor (a menos que también se esté animando inline-size).
    • Los márgenes se rompen en RTL por dos motivos distintos:
    • Firefox posiciona el relleno indeterminado usando solo su margen izquierdo físico, por lo que margin-inline-start no tiene efecto en RTL y el relleno nunca se mueve.
    • En Chromium y WebKit, que disponen el relleno como un bloque normal, una vez que un margen empuja el relleno más allá del final de la pista, los márgenes se suman a más de la pista. El CSS estándar resuelve ese problema ignorando margin-left en RTL, por lo que el segmento salta al borde en lugar de deslizarse fuera de él.
    • translate se aplica después del diseño, por lo que no se aplica ninguno de los problemas.
  • Para duplicar el movimiento en la escritura de derecha a izquierda (como lo hace la barra nativa de Chrome), invierte la animación en :dir(rtl):
progress:dir(rtl) {
  --progress-bar-indeterminate-animation: progress-bar-indeterminate var(--progress-bar-indeterminate-animation-duration) infinite linear reverse;
}
  • Define las propiedades públicas de --progress-bar-indeterminate-* en progress (su nombre ya las limita al estado) para que los consumidores puedan anularlas sin conocer :indeterminate.
  • Para variar la longitud del segmento (p.ej., el borde inicial se adelanta y el borde final lo alcanza), configura --progress-bar-indeterminate-animation en dos animaciones simultáneas: una cuyos fotogramas clave animen translate en cqi (posición) y otra cuyos fotogramas clave animen inline-size (longitud), de modo que cada una obtenga sus propios fotogramas clave y aceleración. NO intentes dar forma a ambos bordes en una sola lista de fotogramas clave; el animation-timing-function por fotograma clave se aplica a todas las propiedades de ese fotograma clave.
  • --progress-bar-animation se aplica en ambos estados y se puede usar para efectos como un brillo determinado. revert-rule se usa para anular solo cuando se establece, para maximizar la componibilidad.
  • Nunca inhabilites la animación indeterminada (animation: none deja un relleno de ancho cero en Chromium). Para reducir el movimiento, disminuye la velocidad.
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 */
}

Consideraciones adicionales de accesibilidad

Además de los lineamientos mencionados anteriormente, ten en cuenta lo siguiente:

  • Anuncia la finalización sin robar el foco: No configures tabindex ni llames a .focus() en <progress>, ya que no es interactivo y mover el foco a él interrumpe a los usuarios del teclado. Para anunciar cuando finaliza una tarea en segundo plano, actualiza un elemento de estado cortés (role="status" o aria-live="polite").
  • No agregues texto de resguardo dentro de <progress>: Los navegadores actualizados ignoran el texto dentro de <progress>, y las tecnologías de accesibilidad no lo usan.
  • Evita aria-busy + aria-describedby en un <div> genérico sin etiqueta. aria-busy tiene una compatibilidad inconsistente con el lector de pantalla, y las tecnologías de accesibilidad ignoran aria-describedby en un <div> sin nombre y sin un rol. Si el contenido de una región no está listo mientras se carga, ocúltalo con hidden o aria-hidden="true".
  • SÍ asegúrate de que haya un contraste adecuado entre la barra de progreso y la pista cuando agregues estilos personalizados.
  • Considera alternativas para representar el estado indeterminado: Las animaciones de la barra de progreso indeterminada que se muestran en paralelo a otro contenido pueden considerarse una distracción para algunos usuarios.

Estrategias de resguardo

Resguardos y compatibilidad de navegadores con accent-color

La propiedad accent-color es una mejora progresiva. Los navegadores que no admitan esta propiedad la ignorarán y usarán los colores predeterminados de su IU.

Si no se admite accent-color y se requiere aplicar estilo a la barra de progreso, usa el método de estilo personalizado para aplicar el estilo preferido a la barra de progreso.

Resguardos y compatibilidad de navegadores para revert-rule

Si no se admite revert-rule, animation: var(--progress-bar-animation, revert-rule) recurre a animation-name: revert-rule con una duración de 0s cuando no se establece --progress-bar-animation, lo que es una operación nula inofensiva cuando no existe @keyframes revert-rule.