Esto es un extra avanzado. Usa una técnica reciente (scroll-driven animations) y no la necesitas para maquetar; pero es de las cosas que mejor demuestran lo lejos que llega el CSS moderno por sí solo.
Si te has fijado, arriba de cada capítulo de este curso hay una barra fina que se
va llenando a medida que lees. No la mueve JavaScript: es CSS puro. Aquí
desmontamos el truco. De paso conoces dos cosas nuevas: las animaciones con
@keyframes y las animaciones ligadas al scroll.
De transition a @keyframes#
En el bonus de acabado visual usaste transition: suaviza el cambio entre dos
estados cuando algo cambia (un :hover). Una animación va más allá: defines
una secuencia con @keyframes y se reproduce sola.
/* define una secuencia llamada "crecer" */
@keyframes crecer {
/* estado inicial de la animación */
from {
/* arranca encogido a nada en horizontal */
transform: scaleX(0);
}
/* estado final de la animación */
to {
/* termina a su tamaño completo */
transform: scaleX(1);
}
}@keyframes crecer describe el viaje: empieza en scaleX(0) y termina en
scaleX(1) (qué hace exactamente scaleX lo vemos en el apartado siguiente). Por
sí solo no hace nada; hay que reproducirlo con la propiedad animation en algún
elemento. (Puedes usar porcentajes —0%, 50%, 100%— en
vez de from/to para tener más etapas.)
transform: scaleX: una barra que crece#
scaleX escala un elemento solo en horizontal: scaleX(1) es su tamaño normal,
scaleX(0) lo encoge a nada, scaleX(0.5) lo deja a la mitad. Si animamos una
barra de scaleX(0) a scaleX(1), crece de vacía a llena.
El detalle que más despista: desde dónde crece. Por defecto, transform toma
como origen el centro del elemento, así que scaleX escala hacia los dos lados. Para
que una barra de progreso crezca desde la izquierda, hay que mover ese origen:
.progreso {
/* borde izquierdo */
transform-origin: 0 50%;
}Atar la animación al scroll: animation-timeline#
Aquí está la magia. Normalmente una animación avanza con el tiempo: su
duration. Pero su “reloj” se puede cambiar. Con animation-timeline: scroll(),
el reloj pasa a ser la barra de scroll: la animación está al 0% cuando estás
arriba del todo y al 100% cuando llegas abajo.
.progreso {
/* reproduce los @keyframes "crecer"; duración auto (la marca el scroll) */
animation: crecer auto linear;
/* el "reloj" es el scroll vertical de la página, no el tiempo */
animation-timeline: scroll(root block);
}Como el avance lo marca el scroll y no el tiempo, la duración se pone en auto.
scroll(root block) dice: “usa el scroll vertical (block) de la página entera
(root)”. Y ya está: la barra se llena exactamente al ritmo al que lees.
La barra, entera#
Juntando las tres piezas —barra fija, animación de scaleX, reloj de scroll—:
.progreso {
/* saca la barra del flujo y la ancla a la ventana */
position: fixed;
/* pegada al borde superior */
top: 0;
/* desde el borde izquierdo... */
left: 0;
/* ...hasta el derecho: ocupa todo el ancho */
right: 0;
/* grosor de la barra */
height: 3px;
/* degradado horizontal de color (el mismo del playground) */
background: linear-gradient(90deg, #b8336a, #6a5af0);
/* estado base: invisible si no hay soporte */
transform: scaleX(0);
/* crece desde la izquierda */
transform-origin: 0 50%;
/* nunca roba un clic */
pointer-events: none;
/* reproduce los @keyframes "crecer" */
animation: crecer auto linear;
/* atado al scroll vertical de la página */
animation-timeline: scroll(root block);
}
@keyframes crecer {
/* scroll arriba del todo: barra vacía */
from { transform: scaleX(0); }
/* scroll abajo del todo: barra llena */
to { transform: scaleX(1); }
}Dos detalles de oficio que separan lo que funciona de lo que está bien hecho:
- El estado base
transform: scaleX(0)es la red de seguridad. Las scroll-driven animations son recientes (Chrome/Edge 115+, Firefox 136+, Safari 26+). En un navegador antiguo,animation-timelinese ignora y la barra se queda con su estilo base. Si ese estilo esscaleX(0), queda invisible —nadie ve algo roto—; si no lo declaras, queda llena y fija. pointer-events: noneyaria-hidden(este último, en el HTML): la barra es decoración. No debe interceptar clics ni anunciarse a un lector de pantalla.
Esto es, casi tal cual, el CSS que pinta la barra de progreso de este mismo curso.
Pruébalo#
Haz scroll dentro del preview y mira la barra llenarse. Luego, en styles.css,
quita la línea transform-origin: 0 50% y vuelve a desplazarte: verás cómo crece
desde el centro, el fallo más típico.
Comprueba lo que sabes#
Pregunta 1 de 4
¿En qué se diferencia una animación con `@keyframes` de una `transition`?
Tu turno#
La página de lectura no da pista de cuánto queda. Conviértela en una con barra de
progreso: completa el TODO de starter/styles.css. Cuando la tengas, despliega las
soluciones y fíjate en el detalle del transform-origin y del estado base.
Ejercicio · en esta página
Construye la barra de progreso de scroll
La página de lectura es larga y no da pista de cuánto queda. Convierte el <div class="progreso"> en una barra que se llene arriba al hacer scroll, con CSS puro: una animación de scaleX atada al scroll con animation-timeline. Necesitas un navegador con scroll-driven animations (Chrome/Edge 115+, Firefox 136+, Safari 26+).
Paso 1: Que reaccione al scroll
- La barra está fija arriba y cambia a medida que haces scroll.
- Usa `@keyframes` de `scaleX(0)` a `scaleX(1)` con `animation-timeline: scroll()`.
Paso 2: Que crezca bien
- `transform-origin: 0 50%`: la barra se llena de izquierda a derecha, no desde el centro.
- Alto y color discretos; queda como una barra de progreso de lectura.
Paso 3: Robusta y limpia
- `transform: scaleX(0)` como estado base: degradación limpia (invisible) donde no hay soporte.
- `pointer-events: none` y `aria-hidden` (en el HTML): la barra es decoración, no roba clics ni la anuncia un lector.
Ver soluciones
/* SOLUCIÓN OK — la barra reacciona al scroll, pero con dos fallos.
Crece desde el centro (falta transform-origin) y no tiene estado base, así que
en navegadores que no soportan scroll() se queda llena todo el rato. */
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.6;
}
.progreso {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #b8336a;
/* Sin transform-origin, scaleX crece desde el CENTRO hacia los lados: raro.
Y sin un transform base, donde no se soporta scroll() la barra se queda en
scaleX(1), es decir, llena y fija. */
animation: progreso-crecer auto linear;
animation-timeline: scroll(root block);
}
@keyframes progreso-crecer {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.cab {
max-width: 38rem;
margin: 0 auto;
padding: 3rem 1.25rem 1rem;
}
.kicker {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #b8336a;
margin: 0 0 0.25rem;
}
.cab h1 {
font-size: 1.8rem;
margin: 0;
}
.articulo {
max-width: 38rem;
margin: 0 auto;
padding: 0 1.25rem 2rem;
}
.articulo h2 {
font-size: 1.2rem;
margin: 2rem 0 0.5rem;
}
.articulo p {
margin: 0 0 1rem;
color: #2b2a33;
}
.pie {
max-width: 38rem;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué este nivel
- El mecanismo está bien: `@keyframes` de `scaleX(0)` a `scaleX(1)` reproducido con `animation-timeline: scroll(root block)`. La barra reacciona al scroll.
- Pero crece desde el centro hacia los lados: falta `transform-origin: 0 50%` para anclarla al borde izquierdo.
- Y sin un `transform` base, en un navegador sin soporte de `scroll()` la barra se queda en `scaleX(1)`: llena y fija, justo lo contrario de lo que quieres.
/* SOLUCIÓN MEJOR — la barra crece bien, desde la izquierda.
El arreglo clave frente al "ok": transform-origin en el borde izquierdo, para
que scaleX llene de izquierda a derecha como una barra de progreso de verdad. */
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.6;
}
.progreso {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #b8336a;
/* El origen en el borde izquierdo: ahora scaleX(0.5) es "media barra desde la
izquierda", no "media barra centrada". */
transform-origin: 0 50%;
animation: progreso-crecer auto linear;
animation-timeline: scroll(root block);
}
@keyframes progreso-crecer {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.cab {
max-width: 38rem;
margin: 0 auto;
padding: 3rem 1.25rem 1rem;
}
.kicker {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #b8336a;
margin: 0 0 0.25rem;
}
.cab h1 {
font-size: 1.8rem;
margin: 0;
}
.articulo {
max-width: 38rem;
margin: 0 auto;
padding: 0 1.25rem 2rem;
}
.articulo h2 {
font-size: 1.2rem;
margin: 2rem 0 0.5rem;
}
.articulo p {
margin: 0 0 1rem;
color: #2b2a33;
}
.pie {
max-width: 38rem;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué es mejor que el anterior
- `transform-origin: 0 50%`: ahora `scaleX` llena de izquierda a derecha. Es una barra de progreso de lectura de verdad.
- El motor es el correcto: la animación no la mueve el tiempo, la mueve el scroll, sin una línea de JavaScript.
- Le falta el último detalle: qué pasa donde no hay soporte para scroll-driven animations.
/* SOLUCIÓN EXCELENTE — robusta y degrada con limpieza.
Es, casi línea por línea, la barra que usa este curso en cada capítulo.
- transform: scaleX(0) como ESTADO BASE: donde no se soporta scroll(), la
animación no corre y la barra se queda invisible, en lugar de llena.
- pointer-events: none, para que nunca robe un clic. (El aria-hidden ya está
en el HTML: para un lector de pantalla, la barra no existe.)
- Un degradado con los dos acentos, en lugar de un color plano. */
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.6;
}
.progreso {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 3px;
z-index: 10;
background: linear-gradient(90deg, #b8336a, #6a5af0);
transform: scaleX(0);
transform-origin: 0 50%;
pointer-events: none;
animation: progreso-crecer auto linear;
animation-timeline: scroll(root block);
}
@keyframes progreso-crecer {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.cab {
max-width: 38rem;
margin: 0 auto;
padding: 3rem 1.25rem 1rem;
}
.kicker {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #b8336a;
margin: 0 0 0.25rem;
}
.cab h1 {
font-size: 1.8rem;
margin: 0;
}
.articulo {
max-width: 38rem;
margin: 0 auto;
padding: 0 1.25rem 2rem;
}
.articulo h2 {
font-size: 1.2rem;
margin: 2rem 0 0.5rem;
}
.articulo p {
margin: 0 0 1rem;
color: #2b2a33;
}
.pie {
max-width: 38rem;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 0.85rem;
} Por qué es mejor que el anterior
- `transform: scaleX(0)` como estado base: donde el navegador no soporta scroll-driven animations, la animación no corre y la barra queda invisible, en vez de llena. Degradación limpia.
- `pointer-events: none` para que la barra no robe un clic, y `aria-hidden` (en el HTML) para que un lector de pantalla la ignore: es pura decoración.
- Es, casi línea por línea, la barra que has visto en cada capítulo de este curso. Ahora sabes cómo está hecha.