Cómo compilar un componente de barra de carga

Una descripción general básica de cómo crear una barra de carga accesible y adaptable al color con el elemento <progress>.

En esta publicación, quiero compartir ideas sobre cómo crear una barra de carga accesible y adaptable al color con el elemento <progress>. Prueba la demostración y consulta la fuente.

Demostración de los estados claro y oscuro, indeterminado, de aumento y de finalización en Chrome.

Si prefieres el formato de video, aquí tienes una versión de YouTube de esta publicación:

Descripción general

El elemento <progress> proporciona comentarios visuales y audibles a los usuarios sobre la finalización. Estos comentarios visuales son valiosos para situaciones como el progreso a través de un formulario, la visualización de información de descarga o carga, o incluso para mostrar que se desconoce la cantidad de progreso, pero el trabajo sigue activo.

Este desafío de GUI funcionó con el elemento HTML <progress> existente para ahorrar un poco de esfuerzo en la accesibilidad. Los colores y los diseños amplían los límites de personalización del elemento integrado para modernizar el componente y hacer que se adapte mejor a los sistemas de diseño.

Pestañas claras y oscuras en cada navegador que proporcionan una descripción general del ícono adaptable de arriba a abajo: Safari, Firefox y Chrome.
Demostración en Firefox, Safari, Safari para iOS, Chrome y Chrome para Android en esquemas claros y oscuros.

Marca

Elegí unir el elemento <progress> en un <label> para poder omitir los atributos de relación explícitos en favor de una relación implícita. También etiqueté un elemento superior afectado por el estado de carga, de modo que las tecnologías de lector de pantalla puedan transmitir esa información al usuario.

<progress></progress>

Si no hay un value, el progreso del elemento es indeterminado. El atributo max se establece de forma predeterminada en 1, por lo que el progreso se encuentra entre 0 y 1. Por ejemplo, si configuras max en 100, el rango se establecerá entre 0 y 100. Elegí mantenerme dentro de los límites de 0 y 1, traduciendo los valores de progreso a 0.5 o 50%.

Progreso con etiqueta

En una relación implícita, un elemento de progreso se une con una etiqueta como esta:

<label>Loading progress<progress></progress></label>

En mi demostración, elegí incluir la etiqueta solo para lectores de pantalla. Para ello, se une el texto de la etiqueta en un <span> y se le aplican algunos estilos para que quede fuera de la pantalla:

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

Con el siguiente CSS complementario de WebAIM:

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

Captura de pantalla de las Herramientas para desarrolladores que revela el elemento de solo lectura de la pantalla.

Área afectada por el progreso de carga

Si tienes una visión saludable, puede ser fácil asociar un indicador de progreso con elementos y áreas de la página relacionados, pero para los usuarios con discapacidad visual, no es tan claro. Para mejorar esto, asigna el atributo aria-busy al elemento superior que cambiará cuando se complete la carga. Además, indica una relación entre el progreso y la zona de carga con aria-describedby.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

Desde JavaScript, cambia aria-busy a true al comienzo de la tarea y a false una vez que finalice.

Adiciones de atributos de Aria

Si bien el rol implícito de un elemento <progress> es progressbar, lo hice explícito para los navegadores que no tienen ese rol implícito. También agregué el atributo indeterminate para colocar explícitamente el elemento en un estado desconocido, lo que es más claro que observar que el elemento no tiene un value establecido.

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

Usa tabindex="-1" para que se pueda enfocar el elemento de progreso desde JavaScript. Esto es importante para la tecnología de lectores de pantalla, ya que, si se enfoca el progreso a medida que cambia, se anunciará al usuario hasta dónde llegó el progreso actualizado.

Estilos

El elemento progress es un poco complicado en lo que respecta al diseño. Los elementos HTML integrados tienen partes ocultas especiales que pueden ser difíciles de seleccionar y, a menudo, solo ofrecen un conjunto limitado de propiedades para configurar.

Diseño

Los estilos de diseño están pensados para permitir cierta flexibilidad en el tamaño del elemento de progreso y la posición de la etiqueta. Se agrega un estado de finalización especial que puede ser un indicador visual adicional útil, pero no obligatorio.

Diseño <progress>

El ancho del elemento de progreso no se modifica para que pueda reducirse y crecer con el espacio necesario en el diseño. Los estilos integrados se quitan estableciendo appearance y border en none. Esto se hace para que el elemento se pueda normalizar en todos los navegadores, ya que cada uno tiene sus propios estilos para su elemento.

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

El valor de 1e3px para _radius usa la notación científica de números para expresar un número grande, por lo que el border-radius siempre se redondea. Equivale a 1000px. Me gusta usar esto porque mi objetivo es usar un valor lo suficientemente grande como para poder establecerlo y olvidarme de él (y es más corto de escribir que 1000px). También es fácil hacerlo aún más grande si es necesario: solo cambia el 3 por un 4, luego 1e4px es equivalente a 10000px.

Se usa overflow: hidden, que es un estilo polémico. Esto facilitó algunas cosas, como no tener que pasar valores de border-radius a los elementos de pista y de relleno de pista, pero también significó que ningún elemento secundario del progreso podía vivir fuera del elemento. Se podría realizar otra iteración en este elemento de progreso personalizado sin overflow: hidden, lo que podría abrir algunas oportunidades para animaciones o mejores estados de finalización.

Progreso completado

Los selectores CSS hacen el trabajo difícil aquí, ya que comparan el valor máximo con el valor actual y, si coinciden, el progreso se completa. Cuando se completa, se genera un seudoelemento y se agrega al final del elemento de progreso, lo que proporciona un buen indicador visual adicional de la finalización.

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

Captura de pantalla de la barra de carga al 100% y con una marca de verificación al final.

Color

El navegador aporta sus propios colores para el elemento de progreso y se adapta a los modos claro y oscuro con una sola propiedad de CSS. Esto se puede desarrollar con algunos selectores especiales específicos del navegador.

Estilos de navegador claro y oscuro

Para que tu sitio use un elemento <progress> adaptable a los modos oscuro y claro, solo se requiere color-scheme.

progress {
  color-scheme: light dark;
}

Color de relleno del progreso de una sola propiedad

Para teñir un elemento <progress>, usa accent-color.

progress {
  accent-color: rebeccapurple;
}

Observa que el color de fondo de la pista cambia de claro a oscuro según el valor de accent-color. El navegador garantiza un contraste adecuado, lo que es genial.

Colores claros y oscuros completamente personalizados

Establece dos propiedades personalizadas en el elemento <progress>: una para el color de la pista y la otra para el color del progreso de la pista. Dentro de la consulta de medios prefers-color-scheme, proporciona valores de color nuevos para la pista y el progreso de la pista.

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

Estilos de enfoque

Anteriormente, le dimos al elemento un índice de tabulación negativo para que se pudiera enfocar de manera programática. Usa :focus-visible para personalizar el enfoque y habilitar el estilo de anillo de enfoque más inteligente. Con esto, un clic del mouse y el enfoque no mostrarán el anillo de enfoque, pero los clics del teclado sí lo harán. El video de YouTube profundiza en este tema y vale la pena revisarlo.

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

Captura de pantalla de la barra de carga con un anillo de enfoque alrededor. Todos los colores coinciden.

Estilos personalizados en todos los navegadores

Personaliza los estilos seleccionando las partes de un elemento <progress> que expone cada navegador. El elemento progress es una sola etiqueta, pero se compone de algunos elementos secundarios que se exponen a través de seudoselectores de CSS. Las Herramientas para desarrolladores de Chrome te mostrarán estos elementos si habilitas el parámetro de configuración:

  1. Haz clic con el botón derecho en tu página y selecciona Inspeccionar elemento para abrir las Herramientas para desarrolladores.
  2. Haz clic en el ícono de ajustes en la esquina superior derecha de la ventana de Herramientas para desarrolladores.
  3. En el encabezado Elementos, busca y habilita la casilla de verificación Mostrar el DOM de sombra del usuario-agente.

Captura de pantalla de dónde habilitar la exposición del DOM de sombra del usuario-agente en las Herramientas para desarrolladores.

Estilos de Safari y Chromium

Los navegadores basados en WebKit, como Safari y Chromium, exponen ::-webkit-progress-bar y ::-webkit-progress-value, que permiten usar un subconjunto de CSS. Por ahora, establece background-color con las propiedades personalizadas que creaste antes, que se adaptan a los temas claro y oscuro.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

Captura de pantalla en la que se muestran los elementos internos del elemento de progreso.

Estilos de Firefox

Firefox solo expone el pseudoselector ::-moz-progress-bar en el elemento <progress>. Esto también significa que no podemos matizar la pista directamente.

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

Captura de pantalla de Firefox y dónde encontrar las partes del elemento de progreso.

Captura de pantalla de Debugging Corner, donde Safari, Safari para iOS, 
  Firefox, Chrome y Chrome para Android muestran la barra de carga en funcionamiento.

Observa que Firefox tiene un color de pista establecido desde accent-color, mientras que Safari para iOS tiene una pista de color azul claro. Lo mismo ocurre en el modo oscuro: Firefox tiene una pista oscura, pero no el color personalizado que establecimos, y funciona en los navegadores basados en Webkit.

Animación

Cuando se trabaja con selectores seudoelementos integrados en el navegador, suele ser con un conjunto limitado de propiedades de CSS permitidas.

Cómo animar el llenado de la pista

Agregar una transición al elemento inline-size del elemento de progreso funciona para Chromium, pero no para Safari. Firefox tampoco usa una propiedad de transición en su ::-moz-progress-bar.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

Cómo animar el estado :indeterminate

Aquí me pongo un poco más creativo para poder proporcionar una animación. Se crea un seudoelemento para Chromium y se aplica un gradiente que se anima de un lado a otro para los tres navegadores.

Las propiedades personalizadas

Las propiedades personalizadas son excelentes para muchas cosas, pero una de mis favoritas es simplemente darle un nombre a un valor de CSS que, de otro modo, parecería mágico. A continuación, se muestra un linear-gradient bastante complejo, pero con un nombre agradable. Su propósito y casos de uso se pueden comprender con claridad.

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

Las propiedades personalizadas también ayudarán a que el código se mantenga DRY, ya que, una vez más, no podemos agrupar estos selectores específicos del navegador.

Los fotogramas clave

El objetivo es una animación infinita que va y viene. Los fotogramas clave de inicio y finalización se establecerán en CSS. Solo se necesita un fotograma clave, el del medio en 50%, para crear una animación que vuelva al punto de partida una y otra vez.

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

Segmentación de cada navegador

No todos los navegadores permiten la creación de seudoelementos en el elemento <progress> ni la animación de la barra de progreso. Más navegadores admiten la animación de la pista que un seudoelemento, por lo que actualizo desde seudoelementos como base y en barras de animación.

Pseudoelemento de Chromium

Chromium permite el pseudoelemento ::after que se usa con una posición para cubrir el elemento. Se usan las propiedades personalizadas indeterminadas, y la animación de ida y vuelta funciona muy bien.

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Barra de progreso de Safari

En Safari, las propiedades personalizadas y una animación se aplican a la barra de progreso del seudoelemento:

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Barra de progreso de Firefox

En Firefox, las propiedades personalizadas y una animación también se aplican a la barra de progreso del seudoelemento:

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

JavaScript desempeña un papel importante con el elemento <progress>. Controla el valor que se envía al elemento y garantiza que haya suficiente información en el documento para los lectores de pantalla.

const state = {
  val: null
}

La demostración ofrece botones para controlar el progreso; actualizan state.val y, luego, llaman a una función para actualizar el DOM.

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

En esta función, se produce la organización de la IU y la UX. Para comenzar, crea una función setProgress(). No se necesitan parámetros porque tiene acceso al objeto state, al elemento de progreso y a la zona <main>.

const setProgress = () => {
  
}

Estableciendo el estado de carga en la zona <main>

Según si el progreso se completó o no, el elemento <main> relacionado necesita una actualización del atributo aria-busy:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

Borra los atributos si se desconoce el importe de carga

Si el valor es desconocido o no se estableció, null en este uso, quita los atributos value y aria-valuenow. Esto hará que el <progress> sea indeterminado.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

Corrige problemas matemáticos decimales de JavaScript

Como elegí mantener el máximo predeterminado de progreso de 1, las funciones de incremento y decremento de la demostración usan operaciones matemáticas decimales. JavaScript y otros lenguajes no siempre son buenos para eso. Aquí tienes una función roundDecimals() que recortará el exceso del resultado matemático:

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

Redondea el valor para que se pueda presentar y sea legible:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

Establece el valor para los lectores de pantalla y el estado del navegador

El valor se usa en tres ubicaciones del DOM:

  1. Es el atributo value del elemento <progress>.
  2. El atributo aria-valuenow.
  3. El contenido de texto interno de <progress>.
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

Cómo enfocar el progreso

Con los valores actualizados, los usuarios videntes verán el cambio de progreso, pero los usuarios de lectores de pantalla aún no recibirán el anuncio del cambio. Enfoca el elemento <progress> y el navegador anunciará la actualización.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

Captura de pantalla de la app de VoiceOver de macOS 
  que lee el progreso de la barra de carga al usuario

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂

Sin duda, hay algunos cambios que me gustaría hacer si tuviera otra oportunidad. Creo que hay margen para limpiar el componente actual y para intentar compilar uno sin las limitaciones de estilo de pseudoclase del elemento <progress>. ¡Vale la pena explorarlo!

Diversifiquemos nuestros enfoques y aprendamos todas las formas de crear en la Web.

Crea una demostración, envíame un tweet con los vínculos y la agregaré a la sección de remixes de la comunidad que aparece a continuación.

Remixes de la comunidad