Stop Doing Algebra in clamp(). CSS progress() Is Here
TL;DR: progress() turns any value into a number between 0 and 1. That makes fluid type and spacing readable, and since September 2026 it works in every major browser. The line nobody wants to touch Every codeb
TL;DR: progress() turns any value into a number between 0 and 1.
That makes fluid type and spacing readable, and since September 2026 it works in every major browser.
The line nobody wants to touch
Every codebase has one:
h1 {
/* 1rem at 360px, 2rem at 1280px. Trust me. */
font-size: clamp(1rem, 0.6087rem + 1.7391vw, 2rem);
}
It works.
It also looks like a lottery ticket.
Where did 0.6087rem come from?
A fluid type calculator, in a tab that closed years ago.
Now your designer moves the breakpoint to 1440px.
Time to redo the algebra.
Meet progress()
progress() answers one question: how far along is this value, between a start and an end?
progress(<value>, <start>, <end>)
Under the hood it is (value - start) / (end - start), clamped to the range 0 to 1.
.demo {
opacity: progress(5, 0, 10); /* 0.5 */
scale: progress(15, 0, 10); /* 1, not 1.5. It clamps. */
}
Think of it as a progress bar for any CSS value.
The fun part: the arguments can be lengths.
progress(100vw, 360px, 1280px) is 0 on a 360px screen, 1 at 1280px and up, and slides smoothly in between.
The same heading, minus the homework
h1 {
/* 1rem, plus up to 1rem more between 360px and 1280px */
font-size: calc(1rem + 1rem * progress(100vw, 360px, 1280px));
}
The breakpoints are in the code.
The sizes are in the code.
No slope, no intercept, and no clamp(), because progress() already clamps.
Designer moves the breakpoint to 1440px?
Change 1280px to 1440px.
Done.
I tested both versions in Chrome 154 at viewport widths from 320px to 1600px.
They match, except the hand-rounded clamp() gives 31.9997px at 1280px, while progress() gives a clean 32px.
Small win, but I'll take it.
Need several things to grow together?
Name the ratio once:
:root {
--fluid: progress(100vw, 360px, 1280px);
}
h1 {
font-size: calc(1rem + 1rem * var(--fluid));
}
.section {
padding-block: calc(2rem + 4rem * var(--fluid));
}
Keep a rem in the base, like the 1rem + above, so text still respects the reader's font size setting.
Cards that size themselves by their container
The viewport is not always the right ruler.
A card in a sidebar should not act like it owns the whole screen.
Container query units work as inputs too:
<div class="card-slot">
<article class="card">
<h2 class="card__title">Release notes</h2>
<p>Everything that shipped this week.</p>
</article>
</div>
.card-slot {
container-type: inline-size;
}
.card {
/* 0 when the slot is 280px or narrower, 1 at 560px or wider */
--size: progress(100cqi, 280px, 560px);
padding: calc(0.75rem + 0.75rem * var(--size));
border-radius: calc(6px + 6px * var(--size));
}
.card__title {
font-size: calc(1.125rem + 0.375rem * var(--size));
}
What Chrome 154 actually computed:
| Slot width | Padding | Radius | Title |
|---|---|---|---|
| 200px | 12px | 6px | 18px |
| 420px | 18px | 9px | 21px |
| 900px | 24px | 12px | 24px |
Notice the wrapper.
Container query units resolve against the nearest ancestor container, so the card can't measure itself.
Somebody else has to hold the tape measure.
Can I use it?
Yes:
- Chrome and Edge 138
- Safari 26
- Firefox 155, which shipped in September 2026 and made
progress()Baseline Newly available
Older browsers won't have it, so keep a fallback (gotcha 5 shows the right way).
Gotchas
1. It returns a number, not a length.
progress() gives you something like 0.4.
Multiply it by a unit inside calc().
2. Don't mix types.
progress(3em, 0px, 100px) is fine, since both are lengths.
progress(3s, 0px, 100px) is invalid, and the browser throws out the whole declaration.
Seconds and pixels are not friends.
3. Same start and end? No explosion.
progress(5, 5, 5) is 0, not a divide-by-zero meltdown.
The spec says so.
4. no-clamp isn't in Chrome yet.
progress(no-clamp 15, 0, 10) should give 1.5.
Firefox 155 and Safari 27 support it, Chrome 154 doesn't, so skip it in production for now.
5. The custom property fallback trap.
This fallback works, because older browsers drop the second line while parsing:
h1 {
font-size: 1.5rem;
font-size: calc(1rem + 1rem * progress(100vw, 360px, 1280px));
}
This one quietly doesn't:
h1 {
font-size: 1.5rem;
font-size: calc(1rem + 1rem * var(--fluid));
}
With var(), the browser accepts the line at parse time, so 1.5rem has already lost.
When --fluid later turns out to be gibberish to an old browser, font-size falls back to its inherited or initial value, not to your fallback.
I checked this by swapping progress() for a made-up function: the plain fallback held at 20px, while the var() version shrank to the parent's 10px.
Fix it with a feature query:
.card {
padding: 1rem;
}
@supports (padding: calc(1px * progress(1, 0, 2))) {
.card {
--size: progress(100cqi, 280px, 560px);
padding: calc(0.75rem + 0.75rem * var(--size));
}
}
Wrap-up
progress() lets your CSS say what you meant: this value, between these two sizes.
Go find the clamp() line in your codebase that nobody dares to touch.
Today's the day.
Sources
- MDN: progress()
- CSS Values and Units Module Level 5: progress()
- web.dev: New to the web platform in September
- MDN browser compat data for progress()
This article was written with the help of AI and checked against the sources above.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.