Volver a las notas

CSS progress(): un solo número mueve todo el diseño

20 de octubre de 2026·
cssprogresscontainerquerieswebdev
Demo en vivoAbrir a pantalla completa

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. .stage es un contenedor de tamaño, así que 100cqi dentro de .player es todo su ancho en línea. progress(100cqi, 220px, 640px) convierte ese ancho en una proporción. Cambia 100cqi por 100vw y 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 usa color-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 --p como entrada. progress(var(--p), .55, .9) convierte el tramo 0,55–0,9 de --p en 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 --p registrada se queda con su initial-value (o con lo que fije un ancestro). La demo comprueba CSS.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 un initial-value que quede bien por sí solo y trata el comportamiento fluido como una mejora.
  • no-clamp todavía no está. La especificación permite que progress(no-clamp 15, 0, 10) devuelva 1.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 en CSS.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 y progress() para todo lo que hay entre medias.

Qué vigilar

  • Que no-clamp llegue 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ían media-progress() y container-progress(); se eliminaron en favor de usar unidades como vw o cqi directamente dentro de progress(), 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.

Fuentes