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.
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.
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;
}

Á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);
}

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;
}

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:
- Haz clic con el botón derecho en tu página y selecciona Inspeccionar elemento para abrir las Herramientas para desarrolladores.
- Haz clic en el ícono de ajustes en la esquina superior derecha de la ventana de Herramientas para desarrolladores.
- En el encabezado Elementos, busca y habilita la casilla de verificación Mostrar el DOM de sombra 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, 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);
}

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);
}


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:
- Es el atributo
valuedel elemento<progress>. - El atributo
aria-valuenow. - 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()
}

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.