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.
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.
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;
}
Á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);
}
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;
}
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:
- Haz clic con el botón derecho en tu página y selecciona Inspect Element para abrir Herramientas para desarrolladores.
- Haz clic en el ícono de configuración 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 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 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);
}
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 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:
- El atributo
value
del elemento<progress>
. - El atributo
aria-valuenow
- 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()
}
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.