Back to notes

CSS progress(): one number drives the whole layout

20 October 2026·
cssprogresscontainerquerieswebdev
Live demoOpen full screen

CSS can now tell you how far along a value is between two others, as a plain number from 0 to 1. That's the progress() function, and since 1 September 2026 it's Baseline newly available: Chrome and Edge 138, Safari 26 and Firefox 155 all ship it.

It sounds modest. In practice it's the missing piece for fluid components: you compute one ratio from, say, the width of a container, and then every property that should "grow with the space" reads from that same number. Drag the card in the demo above and watch the cover, the corners, the gradient, the shadow, the equaliser bars and the extra text all move together. The only JavaScript in it changes the width of the box and prints the value; the morphing is pure CSS.

What progress() returns

The definition in CSS Values and Units Level 5 is short. progress() takes three calculations — the progress value, a start and an end — and returns:

(progress value - start) / (end - start)

clamped to the [0, 1] range. So progress(5, 0, 10) is 0.5, progress(15, 0, 10) is 1 and progress(-5, 0, 10) is 0.

The three arguments can be numbers, dimensions or percentages, and they can even mix units, as long as they're the same type. progress(3em, 0px, 100px) is fine (all lengths, with 3em resolved to its computed size); progress(3s, 0px, 100px) is invalid. Whatever goes in, what comes out is a unitless <number>, which is exactly what you want to multiply other values by.

If you've written fluid sizes with clamp() and a slope you worked out on paper, this is the same idea with the maths moved into the browser. The spec itself notes that progress() is essentially syntactic sugar for a particular calc() pattern — but it's sugar that makes the intent readable.

The pattern from the demo

Here's the core of the demo's CSS, trimmed down:

@property --p {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

.stage { container-type: inline-size; }

.player {
  /* 0 when the container is 220px wide, 1 at 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() of progress(): only shows up in the last stretch */
.extra {
  --reveal: progress(var(--p), .55, .9);
  opacity: var(--reveal);
}

/* each bar fills over its own slice of --p */
.eq i {
  --start: calc(var(--i) * .06);
  height: calc(12% + 88% * progress(var(--p), var(--start), calc(var(--start) + .35)));
}

A few things are going on:

  • 100cqi is the input. .stage is a size container, so 100cqi inside .player is the stage's full inline size. progress(100cqi, 220px, 640px) turns that width into a ratio. Swap 100cqi for 100vw and the same component follows the viewport instead.
  • Everything else is start + range * var(--p). Padding, gap, radius and cover size are all linear interpolations off one number. In the full demo the background also uses color-mix(in oklch, var(--teal) calc(100% - var(--p) * 100%), var(--orange)), so the colour shifts from teal to orange as the card grows.
  • progress() can take --p as its own input. progress(var(--p), .55, .9) remaps the 0.55–0.9 stretch of --p to a fresh 0–1, so the extra line fades in late instead of all at once. Giving each bar a slightly later --start turns the same trick into a staggered reveal.

I checked these numbers in Chromium 141: with the stage at 430px, --p computes to 0.5, the cover to 107px and the radius to 29px, which is what the formula predicts.

Why the @property matters

You could write --p: progress(...) without registering it, and in a flat component it would mostly work. But an unregistered custom property is just a stream of tokens: var(--p) pastes progress(100cqi, 220px, 640px) into every place that uses it, and 100cqi is then resolved against that element's nearest container.

I tested it with a nested container 100px wide: the unregistered version gave 0 there (100px is below the 220px start), while the version registered as <number> kept the 0.5 computed on .player. Registering --p with @property makes the browser compute the number once, on the element that declares it, and pass that number down. It's also what lets the demo read --p with getComputedStyle() and show it.

Support and gotchas

  • Baseline newly available, not widely available. It works in current versions of all major engines, but older browsers don't understand it: a regular property that uses progress() is thrown away at parse time, and a registered --p ends up with its initial-value (or whatever an ancestor sets). The demo checks CSS.supports('width', 'calc(progress(1, 0, 2) * 1px)') and, if that fails, computes the same ratio in JavaScript and writes it to --p. If you'd rather avoid JS, choose an initial-value that looks fine on its own and treat the fluid behaviour as an enhancement.
  • no-clamp isn't there yet. The spec allows progress(no-clamp 15, 0, 10) to return 1.5, but according to MDN's compatibility data only Firefox 155 supports the keyword (Safari has it in Technology Preview). Chromium 141, which I tested with, rejects it in CSS.supports(). For now, assume the result is always clamped.
  • Types have to match. Mixing a time and a length, or a length and a plain number (progress(3em, 0, 100)), makes the function invalid.
  • Start equal to end. The spec defines the clamped result as 0 in that case, so a container query where both bounds collapse won't blow up, but it won't animate either.
  • Discrete changes are still a job for queries. progress() interpolates; it doesn't switch layouts. Changing from a column to a row is still a container query. The two combine well: queries for the jumps, progress() for everything in between.

What to keep an eye on

  • no-clamp reaching Chromium and stable Safari, which would make it practical to extrapolate past the bounds.
  • The spec still has open questions on progress(), including whether it should clamp to 0–100% (issue 11825). Earlier drafts had media-progress() and container-progress(); they were dropped in favour of using units like vw or cqi directly inside progress(), which is the approach the demo uses.
  • The rest of CSS Values 5: the spec describes progress() as an input for other calculations, including the *-mix() notations defined in the same draft.

Sources