Have you ever needed to count elements or sum variables across elements and then use the result as a CSS variable? No JavaScript involved. I'll show you how it can be done.
The solution is kind of insane, but beautiful. It took tens of hours of thinking, trying, and failing—again and again—until it was finally accomplished.
Why would one even bother? It opens up fascinating possibilities:
- Changing layout based on content without needing media or container queries
- Dynamically adjusting cells in grid/flex based on their position in the container
- Determining whether a grid cell is in the first row or not
The Challenge
From the beginning, it was clear that a full-featured solution must be implemented using CSS counter()
. You can count elements with the :nth-child
selector or with the brand new sibling-index()
function. However, both solutions only tell you the index within the parent element, not a sum of values. They cannot propagate the number to their parent element.
CSS counter()
, on the other hand, can count elements or even sum variables. For example, you can define a counter in the body
element and then use counter-increment: var(--any-number);
on any child. Then display the sum in body::after
. The issue is that it can ONLY be shown in ::after
or ::before
pseudo-elements, nowhere else.
The challenge is: How do you turn the text content of an ::after
pseudo-element into a variable?
Solution: Converting Counter Values into Variables
There's a neat, hacky solution to get an element's width in CSS as a CSS variable. We'll explore this technique shortly. First, to leverage this approach, we need to turn the text content value of an ::after
element into its width.
To better understand this concept, let's say we need to set the width of <div>17</div>
to 17px
. After spending ages figuring this out, I discovered it can be done with a custom font and a @counter-style
. Let's examine the latter first:
@counter-style countStyle {
system: additive;
additive-symbols: 10000 'N', 1000 'M', 100 'C', 10 "X", 1 "I";
}
The above defines a counter style that describes how to convert a number to its visual representation on screen. For example, myCounter
with value 2344
in this code:
.counter::after {
content: counter(myCounter, countStyle);
}
will be converted to MMCCCXXXXIIII
. The system works by starting with the highest number in additive-symbols
and checking how many times it fits into 2344
. N
fits 0 times, M
fits 2 times (resulting in MM
), then those 2000
are subtracted from the initial number, leaving us with 344
. This remainder is then tested with 100 'C'
and so on, until the last additive-symbol.
Next, we apply a custom font to MMCCCXXXXIIII
. We define the font only for characters N, M, C, X, and I. I
is set to a 1px
wide space, X
is set to a 10px
wide space, and so on. The font itself is created via Glyphr Studio with a maximum character width set to 10,000 points. Because of this, we need to set the font-size to an astounding 10000px
. Character I
will be displayed as 1px
wide while N
appears as 10000px
wide.
This is the key insight: we now have width derived from text content. An element with MMCCCXXXXIIII
will be exactly 2344px
wide.
Converting Element Width to CSS Variable
Let's return to how to turn element width into a CSS variable. You can read the full article by Temani Afif on this topic: How to Get the Width/Height of Any Element in Only CSS.
The technique requires view-timeline animation and essentially states that if you know 3 of these 4 dimensions, you can calculate the 4th:
- Dimension of the scroller
- Dimension of the subject
- Progress of the animation
- Offset of the subject
We define a ::before
element with width 1px
(subject dimension) and align it to the very left. The animation progress and offset within the container is 0. This means we can calculate the dimensions of the scroller.
Once we know the scroller width, we can use the ::after
element (the one from above with counter value represented by custom font). This time we already know the scroller width, and again the animation progress and offset is 0. We can obtain the width of the subject (counter) as a CSS variable. Because we use the timeline-scope
property, the variable is available on the scroller (parent element).
Example
Please see the code below for the complete solution. Works in Chrome only now!
Limitations
There are several limitations introduced by this implementation:
- Browser Support: Currently only works in Chrome and Safari Technology Preview (no Firefox support)
-
Layout Constraints: Uses
overflow: auto
on the count container. You cannot absolutely position elements outside of it unless using layer APIs like popover -
Size Limitations: Requires defining a child in the container with width equal to the count. If the count exceeds the container width, counting stops working. This can be partially addressed by decreasing font size 10 time, so counter value
1
translates to0.1px
, but this may result in incorrect rounding in some cases
Practical Application: Detecting First Row in Grid
If you need to determine whether a cell is in the first row of a grid, you can sum all the children's spans (since one child can occupy more than one column). Sum these spans using the technique mentioned above to get --total-span-count-in-grid
. Then use clamp()
with the maximum number of columns available in the grid:
--is-first-row: clamp(
0, calc(var(--max-columns) - var(--total-span-count-in-grid)), 1
);
If elements span only across the first row, --is-first-row
equals 1
; otherwise, it equals 0
. Any CSS calculation can then be multiplied by this value to get either 0
or the desired calculation if we're in the first row.
Conclusion
This technique opens up new possibilities for CSS-only solutions to complex layout problems. While it has limitations and browser support constraints, it demonstrates the creative potential of combining CSS counters, custom fonts, and view-timeline animations to achieve what was previously thought impossible without JavaScript.
Top comments (0)