TL;DR: CSS can now divide a length by a length, so calc(100vw / 1px) gives you a plain, unitless number. Firefox 158 shipped it on October 13, 2026, joining Chrome and Safari, so CSS typed arithmetic is Baseline Newly available.
The resize listener that only exists to delete "px"
You want a background image to fade as the viewport gets narrower. Or a heading whose line-height tightens as its container grows. Both need the same thing: a width, as a number.
CSS had the width. It just refused to hand it over without a unit attached. So you wrote this:
const root = document.documentElement; new ResizeObserver(([entry]) => { root.style.setProperty("--w", entry.contentRect.width); }).observe(root); A whole observer, running on every resize, whose only job is to remove two letters. CSS knew the answer the entire time. It just was not allowed to say it.
What it is
The rules for calc() used to be strict: you could divide a length by a number, but not by another length. 200px / 4px was simply invalid, so the declaration was thrown away.
Typed arithmetic relaxes that rule. When you divide a value by another value of the same type, the units cancel and you get a <number>. The units do not have to match, only the type: 100vw / 1px and 100cqi / 1em are both fine.
That number can go anywhere a number is accepted, like opacity, z-index, line-height, or font-weight. Multiply it by 1deg or 1rem and it becomes an angle or a length again.
The minimal one
.box { opacity: calc(100vw / 2000px); } I tested it in Chrome 157 with fixed-width iframes as viewports. At 400, 800, and 1200 px wide, the computed opacity was 0.2, 0.4, and 0.6. Putting calc(100vw / 1px) into z-index gave exactly 400, 800, and 1200. The viewport width, as a number, with no JavaScript involved.
The realistic one: a hero that adapts to its container
.hero { container-type: inline-size; position: relative; } .hero h1 { font-size: 40px; line-height: clamp(1.05, 1.6 - 100cqi / 1600px, 1.4); font-weight: clamp(400, 100cqi / 1.5px, 800); } .hero::before { content: ""; position: absolute; inset: 0; background: url(texture.png); opacity: clamp(0.15, 100cqi / 1200px, 0.6); } 100cqi is the width of the .hero container, so every value follows the component, not the window. Narrow cards get airy, lighter headings and a faint texture. Wide ones get tight, bold headings and a stronger one.
Here is what Chrome 157 computed:
| Container width | line-height | font-weight | Texture opacity |
|---|---|---|---|
| 360 px | 1.375 (55px) | 400 | 0.3 |
| 720 px | 1.15 (46px) | 480 | 0.6 |
| 1200 px | 1.05 (42px) | 800 | 0.6 |
Three properties, three types, one source of truth. The clamps keep each value in a sane range, so nobody gets a heading heavier than the font file on an ultrawide monitor.
Can I use it?
| Where | Status |
|---|---|
| Baseline | Newly available since October 2026 |
| Chrome and Edge | 140 |
| Safari (macOS and iOS) | 26 |
| Firefox | 158 |
| Tested | Chrome 157, where every number in this post was measured |
| Spec | CSS Values and Units Module Level 4 |
Firefox was the last one in, with a single line in its 158 release notes. Older browsers treat a length-by-length division as invalid and drop the whole declaration, which you can use to your advantage (see the last gotcha).
Gotchas
emmeans the element's own font size.calc(100vw / 1em)at a 360 px viewport gave23on an element withfont-size: 16pxand18on one with20px. Same viewport, different answers. That23is also a rounding:z-indexwants an integer, so 22.5 was rounded up.Multiplying two lengths is still invalid. Division got relaxed, multiplication did not, because
px²is not a thing any property accepts.opacity: calc(2px * 3px)was dropped, and the element kept its earlieropacity: 0.25.CSS.supports("width: calc(2px * 3px)")returnedfalse.Dividing by zero does not throw.
opacity: calc(1px / 0px)computed to1. No error, no warning, just a clamped value. If your divisor comes from a custom property, make sure it can never be0px.An unregistered custom property is resolved where it is used, not where it is declared. I set
--w: calc(100cqi / 1px)on an element inside a 300 px container, then read it withz-index: var(--w)inside a nested 100 px container. The result was100, because the text was pasted in andcqifound the nearer container. Registered with@propertyandsyntax: "<number>", the same variable gave300, computed once at the declaration. Pick the one you mean.Ship a fallback, and let the old browsers skip the line. Put a plain value first and the typed one second. Browsers without typed arithmetic drop the second line and keep the first. In Chrome 157, this computed to
0.18at a 360 px viewport, andCSS.supports("opacity: calc(1px / 1px)")returnedtrue:
.legacy { opacity: 0.4; opacity: calc(100vw / 2000px); } @supports (opacity: calc(1px / 1px)) { /* typed arithmetic is available */ } Wrap-up
Every number your layout needed was already sitting in CSS, wearing a unit it could not take off. Find the ResizeObserver that only writes a custom property, and give the job back to calc().
Sources
- New to the web platform in October 2026 (web.dev)
- Firefox 158 release notes for developers (MDN)
- Using CSS typed arithmetic (MDN)
- calc() (MDN)
This article was written with the help of AI and checked against the sources above.