Cómo compilar un componente de barra de carga

Descripción general fundamental de cómo compilar una barra de carga adaptable de color y accesible con el elemento <progress>

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

Se muestran demostraciones claras y oscuras, indeterminadas, crecientes 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 a los usuarios comentarios visuales y auditivos sobre la finalización. Estos comentarios visuales son valiosos para situaciones como el progreso de un formulario, la visualización de información descargada o carga de datos, o incluso mostrar que se desconoce la cantidad de progreso, pero el trabajo aún está activo.

Este desafío de la GUI funcionó con el elemento HTML <progress> existente para ahorrar algo de 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 ajuste 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, Chrome.
Se muestra la demostración en Firefox, Safari, Safari para iOS, Chrome y Android Chrome en esquemas claros y oscuros.

Marca

Elegí unir el elemento <progress> en una <label> para omitir los atributos de relación explícita y priorizar una relación implícita. También etiqueté un elemento superior afectado por el estado de carga para que las tecnologías de lectura de pantalla puedan retransmitir esa información al 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. Por ejemplo, si estableces max en 100, el rango se establecería en 0-100. Decidí mantenerme dentro de los límites de 0 y 1, traduciendo los valores de progreso a 0.5 o 50%.

Progreso unido por etiqueta

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, se une el texto de la etiqueta en un <span> y se le aplican algunos estilos de modo que quede fuera de pantalla:

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

Con el siguiente CSS 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 Herramientas para desarrolladores donde se muestra el elemento de solo pantalla lista.

Á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. 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, activa o desactiva aria-busy a true al comienzo de la tarea y a false una vez finalizada.

Adiciones de atributos de Aria

Si bien la función implícita de un elemento <progress> es progressbar, la hice explícita para los navegadores que no tienen esa función implícita. También se agregó el atributo indeterminate para poner explícitamente el elemento en un estado desconocido, 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, si se enfoca el progreso a medida que cambia, se anunciará al usuario qué tan lejos llegó el progreso actualizado.

Estilos

El elemento de progreso es un poco complicado cuando se trata de estilo. 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.

Diseño

Los diseños de diseño permiten 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 una indicación visual adicional útil, pero no obligatoria.

Diseño de <progress>

El ancho del elemento de progreso no se modifica para que se pueda reducir y aumentar con el espacio necesario en el diseño. Los diseños integrados se quitan si estableces appearance y border como none. Esto se hace para que el elemento se pueda normalizar en todos los navegadores, ya que cada uno tiene sus propios estilos para el 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 notación de número científico a fin de expresar un número grande, de modo que border-radius siempre se redondee. Equivale a 1000px. Me gusta usarlo porque mi objetivo es usar un valor lo suficientemente grande como para poder establecerlo y olvidarlo (y es más corto de escribir que 1000px). También es fácil de agrandarlo aún más si es necesario: solo cambia el 3 a 4, luego 1e4px es equivalente a 10000px.

Se usa overflow: hidden, este es un estilo polémico. Facilitaron algunas cosas, como no tener que pasar valores border-radius al segmento ni hacer un seguimiento de los elementos de relleno. Sin embargo, también significaba que no podían existir elementos secundarios del progreso fuera del elemento. Otra iteración de este elemento de progreso personalizado se puede realizar sin overflow: hidden, y podría abrir algunas oportunidades para animaciones o mejores estados de finalización.

Progreso completado

Los selectores CSS hacen el trabajo difícil aquí cuando comparan el máximo con el valor y, si coinciden, se completa el progreso. Cuando se completa el proceso, se genera un seudoelemento que se agrega al final del elemento de progreso, lo que proporciona una señal visual adicional y agradable 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 que muestra una marca de verificación al final

Color

El navegador aporta sus propios colores para el elemento de progreso y se adapta a las luces y las oscuras con solo una propiedad de CSS. Esto se puede compilar con algunos selectores especiales específicos del navegador.

Estilos claros y oscuros del navegador

Para habilitar tu sitio en un elemento <progress> adaptable oscuro y claro, solo se requiere color-scheme.

progress {
  color-scheme: light dark;
}

Color relleno del progreso de una sola propiedad

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 se asegura de que el contraste sea adecuado: bastante ordenado.

Colores claros y oscuros totalmente personalizados

Configura 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 el progreso del seguimiento.

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 habilitar el estilo de anillo de enfoque más inteligente. Con esto, el 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 a su alrededor. Todos los colores coinciden.

Estilos personalizados en todos los navegadores

Para personalizar los diseños, selecciona las partes de un elemento <progress> que expone cada navegador. El uso del elemento de progreso es una sola etiqueta, pero está formada 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 la configuración:

  1. Haz clic con el botón derecho en tu página y selecciona Inspect Element para abrir Herramientas para desarrolladores.
  2. Haz clic en el ícono de configuración 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 shadow DOM de usuario-agente.

Captura de pantalla de dónde en Herramientas para desarrolladores para habilitar 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 el uso de un subconjunto de CSS. Por ahora, configura background-color con las propiedades personalizadas creadas antes, que se adaptan a las luces y las oscuras.

/*  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 de los elementos de progreso.

Captura de pantalla del rincón de depuración donde Safari, Safari para iOS, 
  Firefox, Chrome y Chrome en Android muestran la barra de carga funcionando.

Ten en cuenta que Firefox tiene un color de seguimiento establecido de accent-color, mientras que Safari para iOS tiene un segmento celeste. Es lo mismo en el modo oscuro: Firefox tiene una pista oscura, pero no el color personalizado que establecimos, y funciona en navegadores basados en Webkit.

Animación

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

Cómo animar el relleno de la pista

Agregar una transición a la 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í 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 tiene animación hacia atrás y hacia adelante 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 lo contrario, sería mágico. A continuación, se muestra una linear-gradient bastante compleja, pero con un buen nombre. Su propósito y sus 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 permanezca DRY, ya que, una vez más, no podemos agrupar estos selectores específicos de un navegador.

Los fotogramas clave

El objetivo es una animación infinita que va y viene. Los fotogramas clave de inicio y final se configurarán en CSS. Solo se necesita un fotograma clave, el del medio, en 50%, para crear una animación que vuelva al punto de inicio 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 pueden animar 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 y a las barras de animación.

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 de atrás y 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 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 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
}

La demostración ofrece botones para controlar el progreso; estos 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()

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>

En función de si el progreso se completa o no, el elemento <main> relacionado necesita una actualización del atributo aria-busy:

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

Borrar atributos si se desconoce la cantidad de carga

Si se desconoce el valor o no se establece, null en este uso, quita los atributos value y aria-valuenow. Esto convertirá <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 elegí quedarme con el máximo predeterminado de progreso de 1, las funciones de incremento y disminución de la demostración usan matemáticas decimales. JavaScript y otros lenguajes no siempre son 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 en el DOM:

  1. El atributo value del elemento <progress>.
  2. El atributo aria-valuenow
  3. El contenido de texto interno <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
}

Enfócate en el progreso

Con los valores actualizados, los usuarios videntes verán el cambio en el progreso, pero los usuarios de lectores de pantalla aún no reciben 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 voz en off de Mac OS 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 🙂

Ciertamente, hay algunos cambios que me gustaría hacer si se les da otra oportunidad. Creo que hay espacio para limpiar el componente actual y para intentar compilar uno sin las limitaciones de estilo de seudoclase del elemento <progress>. Vale la pena explorarla.

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 a continuación.

Remixes de la comunidad