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 ideas sobre cómo compilar una barra de carga accesible y adaptable 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.
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.
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 de unión de etiquetas
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 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
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 indicación visual adicional útil, pero no obligatoria.
Diseño <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 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 de números científicos para expresar un número grande, de modo que border-radius
siempre se redondea. 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 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. 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 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 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 propiedad única
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 garantiza un contraste adecuado: bastante ordenado.
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 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. 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 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 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 el DOM sombreado 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;
}
Cómo animar el 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 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 otro modo, se vería mágico. A continuación, se muestra un linear-gradient
bastante complejo, pero con un buen 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 permanezca 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 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)
}
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á 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 demostración 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
}
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 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.