Cómo compilar un componente de barra de carga

Una descripción general fundamental de cómo compilar una barra de carga adaptable y accesible con el 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 demostración y ve la fuente.

Se realizó una demostración clara y oscura, indeterminada, creciente y de finalización en Chrome.

Si prefieres ver un 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 de un formulario, la visualización de información de descarga o carga, o incluso para demostrar que se desconoce el importe del progreso, 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.
La demostración se muestra en Firefox, Safari, Safari de iOS, Chrome y Chrome para Android en esquemas claros y oscuros.

Marca

Elegí unir el elemento <progress> a una <label> para poder omitir los atributos de relación explícita y reemplazarlos por una relación implícita. También etiqueté un elemento superior afectado por el estado de carga para que las tecnologías de lector de pantalla puedan retransmitir esa información a un usuario.

<progress></progress>

Si no hay value, el progreso del elemento es indeterminado. El valor predeterminado del atributo max es 1, por lo que el progreso está entre 0 y 1. Configurar max en 100, por ejemplo, establecería el rango 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 la siguiente:

<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 una <span> y le aplicas algunos estilos de modo que quede efectivamente fuera de la pantalla:

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

Con el siguiente CSS adjunto 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 elementos y áreas de página relacionados, pero para los usuarios con discapacidad visual, no es tan claro. Mejora esto asignando 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, activa o desactiva aria-busy por true al comienzo de la tarea y por false una vez finalizada la tarea.

Adiciones de atributos ARIA

Si bien la función implícita de un elemento <progress> es progressbar, se establece de forma explícita para los navegadores que no tienen esa función implícita. 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 configurado.

<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 lector de pantalla, ya que enfocarse en el progreso a medida que cambia, le anunciará al usuario hasta qué punto alcanzó 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 señal visual adicional útil, pero no obligatoria.

Diseño de <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 navegador 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. Equivale a 1000px. Me gusta usarlo porque mi objetivo es usar un valor lo suficientemente grande como para poder configurarlo y olvidarlo (y su escritura es más corta que 1000px). También es fácil aumentarlo aún más si es necesario: solo cambia el 3 a un 4, por lo que 1e4px será equivalente a 10000px.

overflow: hidden es un estilo conflictivo. 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. Otra iteración en este elemento de progreso personalizado podría realizarse sin overflow: hidden y podría abrirse 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 máximo con el valor y, si coinciden, el progreso está completo. Cuando se completa, se genera un seudoelemento y se agrega al final del elemento de progreso, lo que proporciona un buen indicador visual adicional para completarlo.

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 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 compilar con algunos selectores especiales específicos del navegador.

Estilos de navegador claro y oscuro

Para habilitar tu sitio en 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 del segmento cambia de claro a oscuro según el accent-color. El navegador está garantizando un contraste adecuado: bastante prolijo.

Colores claros y oscuros completamente personalizados

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

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 pestaña negativo para que pudiera enfocarse de manera programática. Usa :focus-visible para personalizar el enfoque y optar por el estilo de anillo de enfoque más inteligente. Con esto, un clic y un enfoque del mouse no mostrarán el anillo de enfoque, pero los clics del teclado sí. 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. Todos 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 consta de unos pocos elementos secundarios que se exponen mediante seudoselectores CSS. Las Herramientas para desarrolladores de Chrome te mostrarán estos elementos si habilitas la 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 shadow DOM de usuario-agente.

Captura de pantalla de dónde se habilita la exposición del shadow DOM del usuario-agente en 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, configura background-color con las propiedades personalizadas creadas antes, que se adaptan al claro y al oscuro.

/*  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 seudoselector ::-moz-progress-bar en el elemento <progress>. Esto también significa que no podemos cambiar el tono de 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 la esquina de depuración, donde se muestra que la barra de carga funciona con Safari, Safari para iOS, Firefox,
  Chrome y Chrome en Android.

Ten en cuenta que Firefox tiene un color de seguimiento configurado desde accent-color, mientras que Safari para iOS tiene un segmento celeste. Ocurre lo mismo en el modo oscuro: Firefox tiene un segmento oscuro, pero no el color personalizado que configuramos, y funciona en navegadores basados en Webkit.

Animación

Cuando se trabaja con seudoselectores integrados en el navegador, por lo general, esto se hace con un conjunto limitado de propiedades CSS permitidas.

Cómo animar que la pista se llene

Agregar una transición al inline-size del elemento de progreso funciona en Chromium, pero no en 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í puedo poner más creatividad para ofrecer una animación. Se crea un seudoelemento 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 asignar un nombre a un valor de CSS que, de otro modo, se veía mágico. A continuación, se muestra un linear-gradient bastante complejo, pero con un lindo 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 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 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 regrese a donde comenzó, una y otra vez.

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

Orientación a cada navegador

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

Pseudoelemento de Chromium

Chromium permite el seudoelemento: ::after, que se usa con una posición para cubrir el elemento. Se usan las propiedades personalizadas indeterminadas, y la animación hacia atrás y hacia adelante 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 tiene un rol importante con el elemento <progress>. Controla el valor enviado al elemento y garantiza que haya suficiente información en el documento para los lectores de pantalla.

const state = {
  val: null
}

En la demostración, se ofrecen botones para controlar el progreso. En ellos, se actualiza state.val y, luego, se llama a una función para actualizar el DOM.

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

setProgress()

Esta función es donde se produce la organizació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 = () => {
  
}

Configura el estado de carga en la zona <main>

Dependiendo de 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)
}

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á <progress> en "indeterminate".

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 de matemáticas decimales en JavaScript

Como elegí mantener el progreso máximo predeterminado de 1, las funciones de incremento y disminución de la demostración usan matemáticas decimales. JavaScript y otros lenguajes no siempre son excelentes 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 un 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. 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 Voice Over de Mac OS en la que se 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 también hay espacio 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 desarrollar en la Web.

Crea una demostración, twittea vínculos y la agregaré a la sección de remixes de la comunidad que aparece más abajo.

Remixes de la comunidad