Código listo para copiar

Estados de carga

Lo que ve el usuario mientras espera dice tanto como el resultado. Nueve piezas de CSS puro para esa espera: esqueletos que reservan el sitio, indicadores que no mienten sobre el tiempo y progreso que sí se puede medir. Todas accesibles y todas con su respuesta a prefers-reduced-motion.

Esqueletos

Un esqueleto imita la forma de lo que va a llegar: bloques del tamaño real del contenido, con un brillo que los recorre. Evita el salto de maquetación porque reserva el espacio antes de tener los datos.

Líneas de texto

Tarjeta completa

Fila de lista

Indicadores sin porcentaje

Cuando no sabes cuánto falta, la interfaz solo puede decir «sigo trabajando». Estos tres lo dicen de tres formas distintas, y ninguno miente sobre el tiempo que queda.

Cargando

Anillo giratorio

Enviando

Puntos que rebotan

Barra indeterminada

Progreso que se puede medir

Si conoces el porcentaje, enséñalo. Un número real baja la ansiedad de la espera mucho más que cualquier animación, y permite que el lector decida si le da tiempo a hacer otra cosa.

Barra de progreso

72 %

Anillo de porcentaje

Convirtiendo a WAV Todo ocurre en tu navegador

Panel procesando

El esqueleto imita la forma, no el contenido

El error más común es dibujar un esqueleto bonito que no se parece a lo que llega después. Cuando el contenido real aparece, la página da un salto y el lector pierde el punto donde estaba mirando. Un esqueleto sirve exactamente para lo contrario: si las tres barras miden lo que van a medir las tres líneas de texto, la sustitución es invisible.

De ahí las proporciones de estas recetas. La última línea de un párrafo casi nunca llega al margen, así que se queda en el 62 %. El nombre de una fila de lista es más corto que su descripción, así que ocupa el 45 %. Son detalles pequeños que separan un esqueleto que tranquiliza de uno que solo decora.

Cómo usar estas piezas

Copia el CSS de la pieza que necesites y pégalo en tu hoja de estilos. Las recetas de esqueleto comparten la clase base .lz-esq, que lleva el degradado y el brillo: cópiala una vez y reutilízala en todas. Los colores van en hexadecimal para que funcionen tal cual, sin depender de ninguna variable previa; sustitúyelos por los tokens de tu sistema cuando los integres.

Todas las piezas son de CSS puro. Ninguna necesita librería, ni fuente externa, ni imagen: el brillo del esqueleto es un degradado que se desplaza y el anillo de porcentaje es un conic-gradient con un círculo encima haciendo de agujero.

El detalle que casi todo el mundo se salta

Un indicador de carga es, por definición, lo único que se mueve en la pantalla mientras el usuario espera. Eso lo convierte en el peor sitio posible para olvidar prefers-reduced-motion, y sin embargo es donde más se olvida. Revisando paquetes de interfaz publicados es fácil encontrar decenas de animaciones infinitas sin una sola regla de movimiento reducido.

La respuesta correcta no es apagar la animación en todos los casos. Un esqueleto sin brillo sigue comunicando su forma, así que ahí se apaga. Un spinner detenido, en cambio, comunica que la aplicación se colgó: ahí se frena, de 0,8 a 2,4 segundos por vuelta. Y los puntos cambian el salto vertical por un latido de opacidad, que informa sin desplazar nada. La regla de fondo es distinguir el movimiento que decora del movimiento que informa.

Lo mismo pasa con el resplandor del panel de proceso. La receta habitual lo manda detrás con z-index: -1, y entonces desaparece en cuanto el panel vive dentro de un contenedor que tenga fondo propio. Aquí va detrás por orden de pintado: el pseudoelemento primero, el panel después con position: relative. Funciona en cualquier contexto y no depende de qué haya alrededor.

Dónde encajan

Los esqueletos son para listas, tarjetas y artículos: cualquier cosa cuya forma conoces de antemano. Los indicadores sin porcentaje son para operaciones cuya duración no puedes calcular, como una petición a un servidor que no informa del avance. El progreso medible es para todo lo que sí puedes contar: subir un archivo, exportar un audio, procesar una lista de elementos uno por uno.

En este mismo sitio, las herramientas de audio son el caso de libro. Al convertir o exportar un archivo el navegador trabaja durante segundos, y ahí el anillo de porcentaje y el panel de proceso dicen dos cosas a la vez: que queda tanto por delante y que el archivo no ha salido de tu equipo.

Las demás categorías

Esta página es una de las nueve del laboratorio de componentes. Las otras ocho: rejillas bento, botones y controles, datos y métricas, formas y recortes, campos de formulario, microinteracciones, vidrio y neón y tarjetas. Y si buscas generadores en vez de piezas hechas, están en Código web.

Preguntas frecuentes

¿Cuándo uso un esqueleto y cuándo un spinner?

Por el tiempo y por la forma. Si la espera es menor de un segundo, no pongas nada: el parpadeo molesta más que la espera. Entre uno y tres segundos, un spinner. Por encima de tres segundos y cuando ya sabes qué forma tendrá el contenido, un esqueleto, porque además reserva el espacio y evita que la página salte al llegar los datos.

¿Por qué el spinner no se detiene con prefers-reduced-motion?

Porque un spinner parado comunica lo contrario de lo que debe: parece que la aplicación se colgó. La regla de movimiento reducido pide quitar el movimiento decorativo, no el que transmite información. Por eso aquí se frena de 0,8 a 2,4 segundos por vuelta en lugar de pararse, y en los puntos se cambia el salto por un latido de opacidad.

¿Qué diferencia hay entre aria-busy y role="status"?

aria-busy="true" va en el contenedor cuyo contenido se está construyendo y le dice al lector de pantalla que no anuncie los cambios todavía. role="status" va en el propio indicador y sí anuncia su texto, una sola vez y sin interrumpir. Los esqueletos usan el primero, los spinners el segundo.

¿Por qué las barras del esqueleto llevan aria-hidden?

Porque no son contenido: son cajas vacías que imitan una forma. Sin aria-hidden, un lector de pantalla anuncia una lista de elementos sin texto, que es ruido puro. La descripción útil va una sola vez en el contenedor, con aria-label.

¿Cómo muevo el valor de la barra o del anillo desde JavaScript?

Las dos leen una variable CSS, así que basta con escribirla y actualizar el atributo de accesibilidad a la vez: elemento.style.setProperty('--valor', pct + '%') para la barra, sin el símbolo de porcentaje para el anillo, y elemento.setAttribute('aria-valuenow', pct) en ambos. Si solo cambias la variable, la interfaz se ve bien pero miente a quien usa lector de pantalla.

¿El desenfoque del panel procesando afecta al rendimiento?

Poco, porque el filtro se aplica a un pseudoelemento pequeño y no a una superficie grande. Aun así, la animación solo toca la opacidad, que el navegador compone sin recalcular la maquetación. Evita animar el propio valor del blur: eso sí obliga a redibujar en cada fotograma.