←All posts

CSS progress() and alpha() Reach Baseline: Fluid Design Without the Breakpoint Pile-Up

September's Baseline additions include the CSS progress() function and the alpha() color function. Here is how they replace stepped media queries and a swamp of opacity variables, and how to ship them safely.

CSS progress() and alpha() Reach Baseline: Fluid Design Without the Breakpoint Pile-Up

Most production stylesheets contain the same two bits of accumulated clutter: a ladder of media queries that nudge a value up one step at a time, and a column of custom properties like --brand-10, --brand-25, --brand-50 that exist only because there was no way to say "this color, but half as opaque." Two CSS features that landed in Baseline's "Newly available" tier in September 2026 target exactly those problems: progress() and alpha().

Both are small. That is the point. Small primitives that delete code are the best kind of platform feature, and these two are now usable in the latest versions of the major engines. According to web.dev's September 2026 roundup, progress() is supported in Chrome 138, Edge 138, Firefox 155 and Safari 26, while alpha() is supported in Chrome 151, Edge 151, Firefox 155 and Safari 27.

progress(): a normalized position, in one function

progress() answers a simple question: where does a value sit between a start and an end? It takes three arguments and returns a number, clamped between 0 and 1 by default.

.card {
  opacity: progress(100vw, 320px, 1200px);
}

At a 320px-wide viewport that resolves to 0, at 1200px it resolves to 1, and in between it is the proportional position. A viewport around 760px lands near 0.5. Because the result is a plain <number>, you can feed it into other math, which is where it becomes useful.

Fluid sizing without a ladder of breakpoints

The classic approach to scaling a heading is either clamp() with a hand-derived slope, or three media queries that jump between sizes. With progress() the intent reads directly from the code:

:root {
  --t: progress(100vw, 360px, 1280px);
}

h1 {
  font-size: calc(2rem + 1.5rem * var(--t));
}

.section {
  padding-block: calc(2rem + 4rem * var(--t));
}

One variable, defined once, drives every dimension that should scale with the viewport. The "design range" (360px to 1280px here) lives in a single place instead of being baked into several clamp() slope calculations that nobody can reverse-engineer six months later.

Being a number, --t also works as a mixing weight. Multiplying by 100% gives a percentage that color-mix() accepts:

.hero {
  background: color-mix(in oklch, var(--paper), var(--accent) calc(var(--t) * 100%));
}

The no-clamp keyword

By default the output is clamped. If you want it to overshoot below 0 or above 1, there is a no-clamp keyword. Per web.dev's support data, that keyword is available in Firefox 155 and Safari 27, so it lags behind the base function. Treat it as an enhancement and test it in your target browsers before depending on overshoot behavior.

Where it does not replace media queries

progress() produces continuous values for properties that can interpolate. It does not toggle layout structure. If a navigation collapses into a menu or a grid goes from three columns to one, you still want a container query or a media query. Use progress() for the numbers, and conditionals for the structure.

alpha(): change the opacity of a color, not the color itself

The second feature is part of CSS Color Level 5. alpha() takes an origin color and returns it with a new alpha channel, keeping the color channels and color space intact.

.overlay {
  background-color: alpha(var(--brand-color) / 50%);
}

This replaces the pattern of declaring a second custom property per opacity level. Before, a design token file might carry --brand, --brand-80, --brand-50 and --brand-20, and every theme override had to redefine all of them. Now you define --brand once, and the translucent variants are derived at the point of use. Change the brand color in a theme and every translucent surface follows.

It also composes with state styling, which previously needed duplicated literals:

.button:focus-visible {
  outline: 3px solid alpha(var(--brand-color) / 60%);
  outline-offset: 2px;
}

.table tr:hover {
  background-color: alpha(var(--brand-color) / 8%);
}

Shipping this safely

"Newly available" means the feature works in the current versions of the core browsers, not in everything your customers use. Baseline's own guidance is to keep fallbacks where a failure would hurt. For these two features that is cheap.

For alpha(), an unsupported declaration is simply dropped, so a plain declaration placed before it acts as the fallback:

.overlay {
  background-color: rgb(20 20 20 / 50%);          /* fallback */
  background-color: alpha(var(--brand-color) / 50%); /* modern */
}

One caveat: this only works when the value is written directly in the declaration. Once a var() is involved, the browser cannot reject the value at parse time and the declaration becomes invalid at computed-value time instead, which does not fall back to the earlier line. For values built from custom properties, gate the modern version explicitly:

.overlay {
  background-color: rgb(20 20 20 / 50%);
}

@supports (color: alpha(red / 50%)) {
  .overlay {
    background-color: alpha(var(--brand-color) / 50%);
  }
}

The same @supports approach works for progress():

:root {
  --size-h1: 2.5rem; /* fixed fallback */
}

@supports (width: calc(1px * progress(100vw, 320px, 1200px))) {
  :root {
    --size-h1: calc(2rem + 1.5rem * progress(100vw, 360px, 1280px));
  }
}

Keep in mind that compatibility tables can lag behind browser releases. Web.dev's own roundup has small inconsistencies between prose and support data for some of the other September features, so verify the specific versions you care about on MDN or in the Baseline data before you commit to a support policy.

Why this matters beyond tidiness

Replacing stepped breakpoints and token sprawl is not just a readability win. Fewer media query branches and fewer duplicated custom properties mean smaller stylesheets and fewer places for a theme change to silently miss a variant. For teams maintaining a design system across several products, that is real maintenance cost removed, and it shows up in review time and regression bugs rather than in a benchmark.

Takeaways

  • Use progress() to define one normalized viewport variable and derive font sizes, spacing and color mixes from it. Keep media or container queries for structural layout changes.
  • Replace families of --color-NN opacity tokens with alpha() at the point of use, so one brand token drives every translucent variant.
  • Always ship a fallback: a preceding declaration for literal values, @supports when var() is involved.
  • Treat no-clamp as an enhancement; it currently trails the base function in browser support.
  • Check support tables against your actual analytics before dropping fallbacks.

Audit your stylesheet for the two smells described at the top. If you find a breakpoint ladder or a handful of opacity tokens, you have a candidate for a quick, low-risk cleanup.

Sources: New to the web platform in September 2026 (web.dev), September's Baseline haul gives CSS progress(), alpha() and revert-rule in every engine (DEV Community).

←Back to all posts
Next step

Need help shipping software?

Tell us what you're trying to build. A discovery call, a one-page summary within 48 hours, a proposal within a week.

Response · 48h·NDA on request·US contracts only