Cómo compilar un componente de barra de carga

Una descripción general fundamental sobre cómo compilar una barra de carga accesible y adaptable al color con el elemento <progress>.

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

Demostraciones de los estados claro y oscuro, indeterminado, creciente y completado en Chrome.

Si prefieres ver un video, aquí tienes una versión de esta publicación en YouTube:

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 en un formulario, la visualización de información de descarga o carga, o incluso para mostrar que el importe del progreso es desconocido, pero el trabajo aún está activo.

Este desafío de la GUI funcionó con el elemento HTML <progress> existente para ahorrar cierto esfuerzo en la accesibilidad. Los colores y los diseños superan 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 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 lectores de pantalla puedan transmitir esa información a un usuario.

<progress></progress>

Si no hay value, el progreso del elemento es indeterminado. El atributo max tiene el valor predeterminado 1, por lo que el progreso está entre 0 y 1. Por ejemplo, si configuras max en 100, el rango se establecerá en 0-100. Elegí mantenerme dentro de los límites de 0 y 1, y traducir los valores de progreso a 0.5 o 50%.

Progreso de unión de etiquetas

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, une el texto de la etiqueta en un <span> y aplícale 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 muestra el elemento Screen ready only.

Área afectada por el progreso de carga

Si tienes una visión sana, puede ser fácil asociar un indicador de progreso con los elementos relacionados y las áreas de la página, pero para los usuarios con discapacidad visual, no es tan claro. Para mejorar esto, asigna el atributo aria-busy al elemento más alto 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 cuando termines.

Adiciones de atributos ARIA

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

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

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

Estilos

Cuando se trata de diseño, el elemento de progreso es un poco complicado. 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 diseñados para permitir cierta flexibilidad en el tamaño del elemento en progreso y la posición de la etiqueta. Se agrega un estado de finalización especial que puede ser una indicación visual adicional útil, pero no obligatoria.

Diseño <progress>

El ancho del elemento de progreso se deja intacto, de modo que pueda reducirse y aumentar con el espacio necesario en el diseño. Para quitar los diseños integrados, configura 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 de números científicos para expresar un número grande, de modo que border-radius siempre se redondea. Es equivalente a 1000px. Me gusta usar esto porque mi objetivo es usar un valor lo suficientemente grande como para poder configurarlo y olvidarme de él (y es más corto escribirlo que 1000px). También es fácil hacerlo aún más grande si es necesario: solo cambia el 3 por un 4, y 1e4px equivale a 10000px.

Se usa overflow: hidden y ha sido un estilo polémico. Facilitó algunas cosas, como la imposibilidad de pasar valores de border-radius al segmento y los elementos de relleno del seguimiento, pero también significó que ningún elemento secundario del progreso podría permanecer fuera del elemento. Se podría realizar otra iteración de 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 de CSS hacen el trabajo difícil aquí comparando el máximo con el valor y, si coinciden, el progreso se completa. Cuando se completa, se genera un pseudoelemento y se agrega al final del elemento de progreso, lo que proporciona una buena indicación visual adicional para 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 que muestra una marca de verificación al final.

Color

El navegador ofrece sus propios colores para el elemento de progreso y se adapta al claro y al oscuro con solo una 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 tenga un elemento <progress> adaptable oscuro y claro, solo se requiere color-scheme.

progress {
  color-scheme: light dark;
}

Color de relleno del progreso de una propiedad única

Para ajustar el tono de 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 accent-color. El navegador garantiza un contraste adecuado: bastante ordenado.

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 de progreso de la pista. Dentro de la consulta de medios prefers-color-scheme, proporciona nuevos valores de color 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 asignamos 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 analiza este tema con más detalle 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. Los colores coinciden.

Estilos personalizados en los navegadores

Para personalizar los estilos, selecciona las partes de un elemento <progress> que expone cada navegador. El uso del elemento de progreso es una sola etiqueta, pero está compuesta por algunos elementos secundarios que se exponen a través de pseudoselectores 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 configuración ubicado 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 sombreado del usuario-agente.

Captura de pantalla de dónde se debe habilitar en DevTools la exposición del shadow DOM del usuario-agente.

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, configura background-color con las propiedades personalizadas que creaste antes, que se adaptan a los temas claros y oscuros.

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

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

Captura de pantalla que muestra los elementos internos del elemento de progreso.

Estilos de Firefox

Firefox solo expone el selector pseudo ::-moz-progress-bar en el elemento <progress>. Esto también significa que no podemos teñir el segmento 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 del rincón de depuración en la que se muestra que la barra de carga funciona en Safari, Safari para iOS, Firefox, Chrome y Chrome para Android.

Observa que Firefox tiene un color de segmento establecido desde accent-color, mientras que iOS Safari tiene un segmento azul claro. Es lo mismo en el modo oscuro: Firefox tiene una pista oscura, pero no el color personalizado que configuramos, y funciona en navegadores basados en Webkit.

Animación

Cuando se trabaja con selectores pseudo integrados en el navegador, a menudo se usa un conjunto limitado de propiedades CSS permitidas.

Animación del relleno de la pista

Agregar una transición al 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 pseudoelemento para Chromium y se aplica un gradiente que se anima hacia adelante y hacia atrás en 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, se vería mágico. A continuación, se muestra un linear-gradient bastante complejo, pero con un buen nombre. Su propósito y casos de uso se pueden entender 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 permanezca 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 inicial y final se establecerán en CSS. Solo se necesita un fotograma clave, el fotograma clave del medio en 50%, para crear una animación que vuelva a su punto de partida una y otra vez.

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

Segmentación para cada navegador

No todos los navegadores permiten la creación de pseudoelementos 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 pseudoelemento, por lo que actualizo de los pseudoelementos como base a las 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 pseudoelemento:

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 pseudoelemento:

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 orquestación de la IU/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 = () => {
  
}

Cómo configurar el estado de carga en la zona <main>

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

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

Borrar los atributos si se desconoce el importe de la carga

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

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

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

Soluciona problemas matemáticos decimales de JavaScript

Como decidí mantener el máximo predeterminado de progreso de 1, las funciones de incremento y decremento de la demostración usan matemáticas decimales. JavaScript y otros lenguajes no siempre son muy buenos en eso. Esta es 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. El atributo value del elemento <progress>
  2. El atributo aria-valuenow
  3. Es 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
}

Priorizar 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 para 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?‽ 🙂

Si tienes otra oportunidad, me gustaría hacer algunos cambios. Creo que hay espacio para limpiar el componente actual y para intentar compilar uno sin las limitaciones de diseño de la pseudoclase del elemento <progress>. Vale la pena explorarlo.

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

Crea una demo, twittea los vínculos y los agregaré a la sección de remixes de la comunidad a continuación.

Remixes de la comunidad