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.
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.](https://web.dev/static/articles/building/a-loading-bar-component/image/light-dark-tabs-each-br-cd04389a437d1.png?authuser=6&hl=es)
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;
}
Á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);
}
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;
}
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:
- 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 configuración ubicado 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 shadow DOM de 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 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);
}
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);
}
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:
- El atributo
value
del 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
}
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()
}
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.