1. Layout computation
Browsers run highly optimized algorithms every frame just to place boxes. That’s the point of layout.
When an auto column finds the widest element, we can think of it as a maximum. A flex row adds up its children’s widths into a sum. order sorts items by a number. Each answer ends up as a size or a position on the page, where modern CSS can read it back as a value.
1.1. Reading a width
A grid item stretches to fill its column by default. If we give it container-type: inline-size, it becomes a query container: an element whose contents can read its width, and that its own contents never widen.
Inside this special element, 100cqw represents the column’s width as a length, like 300px. Dividing a length by 1px drops the unit and round() makes it whole. This is a number we can use to compute in CSS!
<div class="read">
<i style="--value: 300">300</i>
<b class="column-width">width = <output></output></b>
</div>
@property --result { syntax: '<integer>'; inherits: true; initial-value: 0; }
.column-width { display: block; container-type: inline-size; }
.column-width > * { --result: round(100cqw / 1px); }
/* content can't print a number, but a counter can */
output::after { counter-reset: result var(--result); content: counter(result); }
/* the example: one auto column, each item as wide as its --value */
.read { display: inline-grid; grid-template-columns: auto; }
.read > i { width: calc(var(--value) * 1px); }
Registered with @property as an <integer>, the custom property holding it computes to that number where it is set, so the elements inside inherit 300, not 100cqw to be measured again; a counter prints it.
1.1.1. Reading a position
A width can also be a distance. Stretch an element from its parent’s left edge to another element with right: anchor(--target left), and a query container inside it reads how far away that element landed. Here the target sits after two bars in a flex row, so the distance is their sum.
1.2. Finding the widest
If we put three elements in a grid with one auto column, that column grows to fit the widest, so its width is max(12, 40, 30), and the query container in it reads 40. We never wrote a max in calc(). The grid algorithm had to find one anyway, just to lay the elements out, and we borrow that calculation.
1.3. Nesting calculations
We can nest calculations as we do elements. For example, a flex row that hugs its content is as wide as its items added up, so each row becomes a sum. An auto column holding the rows takes the widest of them, so its query container reads max(12 + 40, 30 + 7 + 9), which is 52.
1.4. Feeding it back
Once layout gives us a number, we can use it again as a width, an order, a grid-column, or on another calculation. As usual, there’s one catch: 100cqw measures the nearest query container, so whatever uses the result has to live inside the element that read it. The markup ends up with three levels, the calculation, the query container and the elements inside it.
1.5. Comparing elements
Some frontend tasks require that we compare everything with everything, like ranking a list. Plain calc() can handle it, as long as we write every comparison: for fifty numbers that’s 50 formulas of 49 comparisons each, 2,450 in total. The newer min() and max() get it down to 403, but the formulas grow with the number of items.
Layout can do the comparing for us. If each item’s number is its order, the browser lays them out from smallest to largest, with no formula per pair. Then we can combine it with calc() for the rest of the math. Ranking 300 numbers took 95 ms in calc(), comparing every pair, and 6 ms in layout (§2.3).
2. Primitives
A primitive is a calculation that layout performs when it places elements, such as a sum, a maximum or a sort. Comparing is where plain calc() struggles most, so the two examples below, a sort and a rank, let the order property do it. The API section has many more.
2.1. Sort
When each item’s value is also its order, the grid lines them up from smallest to largest. A query container under the middle column (3 of 5) then reads the median. order only takes whole numbers.
Tick the Last input and the last value drops below the middle, so the median becomes 150.
2.2. Rank
A rank is how many items come before one once they’re sorted. Sorted by order into 100px columns, an item sits 100px further right for every item before it. An element stretched from the grid’s left edge to that item, an anchor, is as wide as that distance, and its query container divides it by 100px to read the rank. Of two equal values, the one earlier in the markup comes first.
When the last value drops from 70 to 50, three items come before 61.
2.3. Performance
Performance is notoriously hard to test, but calc() plus layout wins on some clearly hard cases. Each primitive was timed in Chrome on 300 numbers that change at every update, in plain calc() and with layout helping it.
| Primitive | Plain calc()
| calc() plus layout
|
|---|---|---|
| Atitem i, for a computed i | 99.1 ms, 31 KB of CSS | 3.9 ms |
| Rankeach number’s place, sorted | 95 ms, 5.0 MB of CSS | 6 ms |
| Sortthe numbers in order | 36.8 ms, 1.8 MB of CSS | 3.2 ms |
| Line countlines a row takes as it wraps | 3 ms, 101 KB of CSS | 1 ms |
| Equal growth with a capa grid’s width shared by capped tracks | 2.9 ms, 3 KB of CSS | 1.8 ms |
| Prefix sumwhere each one starts | 0.7 ms, 36 KB of CSS | 0.8 ms |
3. Data-driven stylesheets
d3 is an extremely powerful JavaScript library, with modules for scales, shapes, layouts and statistics. It stands for Data-Driven Documents: bind data to the page and let it decide what appears. I’ve always been drawn to it.
A data-driven stylesheet attempts to do the same with just CSS: the numbers live in custom properties, which are computed by markup into sizes, positions and colors. Layout supplies the algorithm, and the HTML the dataset. Ideally, adding an element adds work for the browser, and not a term to a formula.
That is the goal of dCSS, an open source library of data functions, each one a class you put on HTML. Each function is valid, semantic HTML, and its result comes from layout, or from calc() where arithmetic is enough.
3.1. Using a function
A dCSS function is a class you put on a <figure> element. Each number is an <li> with its --value, and each empty <output> after the list prints one answer, such as --dcss-mean, formatted by dcss-format-fixed to its --precision. Since a screen reader can’t read a custom property, the list is aria-hidden and the <figcaption> gives the numbers in words, so it stays accessible.
<link rel="stylesheet" href="dcss.css">
<link rel="stylesheet" href="families/array.css">
<link rel="stylesheet" href="families/format.css">
<figure class="dcss-summarize dcss-format-fixed">
<ul aria-hidden="true">
<li style="--value: 12"></li>
<li style="--value: 40"></li>
<li style="--value: 30"></li>
</ul>
<output style="--value: var(--dcss-sum); --precision: 0"></output> <!-- 82 -->
<output style="--value: var(--dcss-mean); --precision: 1"></output> <!-- 27.3 -->
<output style="--value: var(--dcss-max); --precision: 0"></output> <!-- 40 -->
<figcaption>The sum, mean and largest of 12, 40 and 30</figcaption>
</figure>
3.2. Drawing a chart
Let’s use this pattern to build something concrete. We can visualize how much of the web uses six CSS layout properties, month by month since 2016. A registered number runs from the first month to the last, and each property’s share is a keyframe per month, so the browser works out every value in between. The lines are drawn once, as SVG, and a clip-path uncovers them as the months go by.
As the lines climb, the y-axis grows to fit them. That is the part layout computes, with dcss-max: each property is one <li>, its --value the highest share it has reached so far. The plot takes the largest, adds a tenth so no line touches the top, and scales the gridlines and the lines to it.
4. Browser support
| Feature | Baseline | Chrome | Safari | Firefox |
|---|---|---|---|---|
| Container query units | Widely available | 105+ | 16+ | 110+ |
:has()
| Widely available | 105+ | 15.4+ | 121+ |
@property
| Newly available 2024 | 85+ | 16.4+ | 128+ |
Math functions: round(), mod(), abs(), sign(), sin()
| Newly available 2025 | 138+ | 15.4+ | 118+ |
anchor-name and anchor()
| Newly available 2026 | 125+ | 26+ | 147+ |
| Style queries on custom properties | Newly available 2026 | 111+ | 18+ | 151+ |
sibling-index()
| Newly available 2026 | 138+ | 26.2+ | 154+ |
View timelines, with timeline-scope
| Newly available 2026 | 116+ | 26+ | 159+ |
Dividing a length by a length (100cqw / 1px)
| No Baseline entry | 140+ | 26+ | 158+ |
Until now, a page without JavaScript mostly flipped switches: a checkbox, a :target, a hover. With the layout engine’s math on our side, a stylesheet can run what we usually reached for a script for, and it gets even more powerful paired with Progressive Enhancement.
Given enough elements and time, CSS can compute anything.