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:
100cqiis the input..stageis a size container, so100cqiinside.playeris the stage's full inline size.progress(100cqi, 220px, 640px)turns that width into a ratio. Swap100cqifor100vwand 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 usescolor-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--pas its own input.progress(var(--p), .55, .9)remaps the 0.55–0.9 stretch of--pto a fresh 0–1, so the extra line fades in late instead of all at once. Giving each bar a slightly later--startturns 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--pends up with itsinitial-value(or whatever an ancestor sets). The demo checksCSS.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 aninitial-valuethat looks fine on its own and treat the fluid behaviour as an enhancement. no-clampisn't there yet. The spec allowsprogress(no-clamp 15, 0, 10)to return1.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 inCSS.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
0in 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-clampreaching 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 hadmedia-progress()andcontainer-progress(); they were dropped in favour of using units likevworcqidirectly insideprogress(), 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.