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 mi pensamiento sobre cómo compilar una barra de carga accesible y adaptativa al color con el elemento <progress>
. Prueba la
demo y consulta la fuente.
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.
En este Desafío de GUI, se trabajó con el elemento HTML <progress>
existente para ahorrar 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 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 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 unido a la 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, 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;
}
Á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 de 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
El elemento de progreso 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 diseñados 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 una indicación visual adicional útil, pero no obligatoria.
Diseño <progress>
El ancho del elemento de progreso no se modifica para que pueda reducirse y aumentar con el espacio necesario en el diseño. Los estilos integrados se quitan si se configuran 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 para expresar un número grande, de modo que border-radius
siempre se redondee. 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 tareas, como no tener que pasar valores border-radius
a la pista y seguir los elementos de relleno, pero también significó que ningún elemento secundario del progreso pudiera vivir 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);
}
Color
El navegador tiene sus propios colores para el elemento de progreso y se adapta a los modos claro y oscuro con solo una propiedad 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 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 accent-color
. El navegador garantiza un contraste adecuado: bastante ordenado.
Colores claros y oscuros totalmente 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. Te recomendamos que mires el video de YouTube para obtener más información.
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}
Estilos personalizados en todos 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:
- Haz clic con el botón derecho en la 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 DevTools.
- En el encabezado Elementos, busca y habilita la casilla de verificación Mostrar los nodos 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);
}
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);
}
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;
}
Animación del 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 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, se vería mágico. A continuación, se muestra un linear-gradient
bastante complejo, pero con un buen nombre. Se pueden comprender claramente su propósito y sus casos de uso.
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 crear una animación infinita que vaya y venga. Los fotogramas clave de inicio y fin 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)
}
Borra los atributos si se desconoce la cantidad de 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 demo 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:
- El atributo
value
del elemento<progress>
- El atributo
aria-valuenow
- 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
}
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 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()
}
Conclusión
Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂
Si tuviera 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.