magnificode
css, typography

progress(): the number fluid type was hiding

Fluid typography has always been one formula. Mike Riethmuller wrote it down in 2015 and the rest of us have been pasting calc(12px + (24 - 12) * ((100vw - 400px) / (800 - 400))) ever since. It works. It's also a mess, and the whole trick is hiding in the second half. (100vw - 400px) / (800 - 400) turns "how far is the viewport between 400 and 800" into a fraction from 0 to 1 (well, 0px to 1px, which is why the other numbers had to lose their units). Everything else is just multiplying that fraction by something.

progress() is that number as a function. Give it a value and two bounds, get back a plain number.

.card h3 {
	--p: progress(100cqi, 240px, 600px);
	font-size: calc(1.25rem + var(--p) * 1.75rem);
}

At 240px wide --p is 0. At 600px it's 1. At 420px it's 0.5. Pair it with 100cqi and the card scales with its own container instead of the viewport.

The part I like is that the number isn't tied to font-size. Compute it once, and use it everywhere. A letter-spacing that tightens as the card grows, a color-mix() percentage, a bar's scale.

It also clamps. progress(125, 0, 100) is 1, not 1.25, and progress(-25, 0, 100) is 0. The working group went back and forth on it before settling on clamped by default in June 2025, and the opt-out, progress(no-clamp 125, 0, 100), took another year to land in the spec. Firefox 155 and Safari 27 have it. Chrome doesn't yet, so for now assume every progress() comes wrapped in a clamp(0, …, 1). (Safari 26's release notes said it wouldn't clamp. CSS-Tricks tested it and it does.)

Two more things. The three arguments have to be the same kind of value, so progress(3em, 0px, 100px) is fine and progress(3em, 0, 100) is invalid. And the result is unitless whatever you feed it, which quietly retires the tan(atan2()) hack for stripping units off a length.

Firefox 155 shipped it Sept 1 and completed the set. Chrome and Edge have had it since 138 (June 2025) and Safari since 26 (September 2025), so MDN marks progress() Baseline 2026 Newly Available. Older browsers drop the declaration. Register --p with @property and an initial-value: 0, and they just render the small end of every range.

The demo has a width slider. Drag it and watch the output, the heading, the color, and the meter all follow one number. The second slider pushes a value past its bounds so you can see the clamp, and the no-clamp version if your browser has it.

CSS progress()

One number, every property

The slider only changes a container’s width. progress() turns that width into a number from 0 to 1, and the CSS spends it on four properties.

Supported: drag the width slider and the heading, color, meter, and readout all follow one progress() value.

This browser does not support progress() yet. Every range sits at its small end and the readouts show a dash.

Fluid everything, no algebra

inline-size: 420px
420px

Fluid heading

progress(100cqi, 240px, 600px) = —

The dashed box is a container, so 100cqi is its inline size. The same number drives the type, the color, and the bar. When the card is wider than this column, the stage scrolls sideways.

Past the bounds

--v: 125
125
progress(var(--v), 0, 100)
—
progress(no-clamp var(--v), 0, 100)no-clamp isn’t in this browser yet
—

The working group chose clamped as the default; no-clamp is the opt-out.