CSS ya puede decirte cuánto ha avanzado un valor entre otros dos, como un número sin unidades de 0 a 1. Es la función progress(), y desde el 1 de septiembre de 2026 es Baseline (newly available): la tienen Chrome y Edge 138, Safari 26 y Firefox 155.
Parece poca cosa. En la práctica es la pieza que faltaba para los componentes fluidos: calculas una proporción a partir de, por ejemplo, el ancho de un contenedor, y todas las propiedades que deben "crecer con el espacio" leen ese mismo número. Arrastra la tarjeta de la demo de arriba y verás cómo la portada, las esquinas, el degradado, la sombra, las barras del ecualizador y el texto extra se mueven a la vez. El único JavaScript que lleva cambia el ancho de la caja y enseña el valor; la transformación es CSS puro.
Qué devuelve progress()
La definición en CSS Values and Units Level 5 es corta. progress() recibe tres cálculos —el valor de progreso, un inicio y un final— y devuelve:
(progress value - start) / (end - start)
limitado al rango [0, 1]. Así, progress(5, 0, 10) vale 0.5, progress(15, 0, 10) vale 1 y progress(-5, 0, 10) vale 0.
Los tres argumentos pueden ser números, dimensiones o porcentajes, e incluso mezclar unidades, siempre que sean del mismo tipo. progress(3em, 0px, 100px) es válido (todo son longitudes y 3em se resuelve a su tamaño calculado); progress(3s, 0px, 100px) no lo es. Entre lo que entre, lo que sale es un <number> sin unidades, que es justo lo que quieres para multiplicar otros valores.
Si alguna vez has hecho tamaños fluidos con clamp() y una pendiente calculada en un papel, esto es la misma idea con las cuentas dentro del navegador. La propia especificación dice que progress() es básicamente azúcar sintáctico para un patrón concreto de calc(), pero es un azúcar que deja clara la intención.
El patrón de la demo
Este es el núcleo del CSS de la demo, recortado:
@property --p {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
.stage { container-type: inline-size; }
.player {
/* 0 cuando el contenedor mide 220px, 1 a 640px */
--p: progress(100cqi, 220px, 640px);
gap: calc(10px + 18px * var(--p));
padding: calc(10px + 18px * var(--p));
border-radius: calc(36px - 14px * var(--p));
}
.cover { width: calc(52px + 110px * var(--p)); }
/* progress() de progress(): solo aparece en el último tramo */
.extra {
--reveal: progress(var(--p), .55, .9);
opacity: var(--reveal);
}
/* cada barra se llena en su propio tramo de --p */
.eq i {
--start: calc(var(--i) * .06);
height: calc(12% + 88% * progress(var(--p), var(--start), calc(var(--start) + .35)));
}
Pasan varias cosas:
- La entrada es
100cqi..stagees un contenedor de tamaño, así que100cqidentro de.playeres todo su ancho en línea.progress(100cqi, 220px, 640px)convierte ese ancho en una proporción. Cambia100cqipor100vwy el mismo componente seguirá al viewport. - Todo lo demás es
inicio + rango * var(--p). Padding, gap, radio y tamaño de la portada son interpolaciones lineales a partir de un único número. En la demo completa el fondo también usacolor-mix(in oklch, var(--teal) calc(100% - var(--p) * 100%), var(--orange)), y el color pasa del verde azulado al naranja a medida que la tarjeta crece. progress()puede usar--pcomo entrada.progress(var(--p), .55, .9)convierte el tramo 0,55–0,9 de--pen un 0–1 nuevo, de modo que la línea extra aparece tarde y no de golpe. Si cada barra empieza un poco más tarde (--start), el mismo truco da una aparición escalonada.
He comprobado estas cifras en Chromium 141: con el escenario a 430px, --p vale 0.5, la portada 107px y el radio 29px, lo que predice la fórmula.
Por qué importa el @property
Podrías escribir --p: progress(...) sin registrarla y, en un componente plano, casi siempre funcionaría. Pero una propiedad personalizada sin registrar es solo una secuencia de tokens: var(--p) pega progress(100cqi, 220px, 640px) en cada sitio donde se usa, y 100cqi se resuelve entonces contra el contenedor más cercano de ese elemento.
Lo he probado con un contenedor anidado de 100px: la versión sin registrar daba 0 ahí (100px está por debajo del inicio de 220px), mientras que la registrada como <number> mantenía el 0.5 calculado en .player. Registrar --p con @property hace que el navegador calcule el número una vez, en el elemento que lo declara, y lo herede hacia abajo. Además, es lo que permite a la demo leer --p con getComputedStyle() y mostrarlo.
Soporte y trampas
- Baseline newly available, no widely available. Funciona en las versiones actuales de todos los motores principales, pero los navegadores antiguos no lo entienden: una propiedad normal que use
progress()se descarta al parsear, y una--pregistrada se queda con suinitial-value(o con lo que fije un ancestro). La demo compruebaCSS.supports('width', 'calc(progress(1, 0, 2) * 1px)')y, si falla, calcula la misma proporción en JavaScript y la escribe en--p. Si prefieres no usar JS, elige uninitial-valueque quede bien por sí solo y trata el comportamiento fluido como una mejora. no-clamptodavía no está. La especificación permite queprogress(no-clamp 15, 0, 10)devuelva1.5, pero según los datos de compatibilidad de MDN solo Firefox 155 soporta esa palabra clave (Safari la tiene en Technology Preview). Chromium 141, con el que he probado, la rechaza enCSS.supports(). Por ahora, da por hecho que el resultado siempre está limitado.- Los tipos tienen que coincidir. Mezclar un tiempo con una longitud, o una longitud con un número sin unidades (
progress(3em, 0, 100)), invalida la función. - Inicio igual a final. La especificación define en ese caso el resultado limitado como
0, así que si los dos límites coinciden no se rompe nada, pero tampoco se mueve. - Los cambios discretos siguen siendo cosa de las queries.
progress()interpola; no cambia de layout. Pasar de columna a fila sigue siendo una container query. Se combinan bien: queries para los saltos yprogress()para todo lo que hay entre medias.
Qué vigilar
- Que
no-clampllegue a Chromium y a Safari estable, lo que permitiría extrapolar más allá de los límites. - La especificación aún tiene preguntas abiertas sobre
progress(), entre ellas si debería limitarse a 0–100% (issue 11825). Los borradores anteriores teníanmedia-progress()ycontainer-progress(); se eliminaron en favor de usar unidades comovwocqidirectamente dentro deprogress(), que es lo que hace la demo. - El resto de CSS Values 5: la especificación presenta
progress()como entrada para otros cálculos, incluidas las notaciones*-mix()definidas en el mismo borrador.