The day CSS made me learn algebra again, and I liked it - Schalk Neethling - Open Web Engineer
Skip to navigation<br>Skip to main content<br>The problem that started it all
I was building a carousel. Each slide had a photo, and on top of the photo, an overlay with a pull-quote and the author’s name. The structure was the usual suspect:
div class="slide-mediaWrapper"><br>img class="slide-media" src="..." /><br>div class="slide-overlay">div><br>div><br>The wrapper used grid, and the image and overlay were stacked via grid-area: 1 / 1 — a common technique for overlaying content on media. Clean markup, no absolute positioning.
Then I looked at it in the browser.
The overlay was overshooting the image. The overlay’s bottom edge was hanging below the photo, and I went through all the usual suspects — max-content, fit-content, inline-grid on the wrapper — chasing the idea that grid’s auto tracks were somehow sizing wrong. None of them changed anything. I convinced myself for a while that grid was the wrong tool and I’d need to rebuild the overlay with absolute positioning.
Then I inspected the image element. It had a margin-block-end on it. The overlay wasn’t extending past the image at all — the image had a margin pushing its bounding box out past its rendered content, and the grid cell was sizing to include that margin, so the overlay (sharing the cell) had more room than the image did.
I moved the margin from the to the wrapper. The overlay snapped into alignment. No absolute positioning needed. No grid rewrite. A stray margin had masqueraded as a layout problem for the better part of an hour.
Lesson filed: when “an element is overflowing its container,” always inspect the element and its immediate siblings for margins before reaching for layout rewrites. The reason this caught me out is that grid auto-track sizing uses each item’s intrinsic size contribution, and the CSS Box Sizing spec is explicit about what that means: “Intrinsic size contributions are based on the outer size of the box; for this purpose auto margins are treated as zero.” In other words, non-auto margins are part of the size the item contributes to its track. The margin-block-end on the image was being added to the image’s contribution, the track grew to fit the image-plus-margin, and the overlay (sharing the cell) inherited that taller cell.
With the block-axis overshoot gone, I noticed a second issue: the image wasn’t filling the wrapper’s inline size. There was empty space on either side.
I inspected the image’s computed styles. It had max-inline-size: 100% from the stylesheet reset (the classic responsive-image rule) but no inline-size. That’s a ceiling, not a target — and that’s where the gap came from. With inline-size (i.e. width) at its default auto, the CSS 2.2 spec is explicit about what happens for a replaced element with intrinsic dimensions: “if 'width' has a computed value of 'auto', and the element has an intrinsic width, then that intrinsic width is the used value of 'width'.” So the resolved to its file’s natural pixel width. That width happened to be smaller than the wrapper, so it sat there at intrinsic, and max-inline-size: 100% had nothing to clamp — the ceiling only kicks in when the image is bigger than the container, not when it’s smaller.
The fix was a one-liner override, replacing the ceiling with an explicit target:
.slide-media {<br>inline-size: 100%; /* was only max-inline-size in the reset */<br>block-size: auto;<br>With that, the image filled the wrapper and kept its aspect ratio. No cropping needed, so object-fit remained unnecessary.
At this point I had a clean slide: image filling the wrapper, overlay bounded to the image. I thought I was done. Then I resized the carousel.
This is where the real story starts.
Fluid typography, and why viewports are the wrong axis
The existing setup used viewport-driven breakpoints: the font jumped from 2.25rem down to 1.625rem somewhere around mobile width. That works fine in most cases, but it was not the case here.
The change in size of the carousel slides was not really tied to the viewport. It was a box inside a box inside a layout. In one context, the slide was 432px wide; in another, 252px. The viewport told me nothing useful about how much room the overlay actually had.
The right tool here is container queries. Specifically, the cqi unit - 1% of the container’s inline size. With container-type: inline-size on the wrapper, cqi lets children scale relative to the slide, not the screen.
And the idiomatic way to make type scale fluidly is clamp():
font-size: clamp(MIN, preferred, MAX);<br>MIN and MAX are easy: those are floors and ceilings from the design system. The hard part is the middle expression — the preferred value. I’d seen this pattern many times:
font-size: clamp(1.625rem, 1.125rem + 2cqi, 2.25rem);<br>And I’d always treated it as a kind of incantation. Slope, interpolation, vibe. If it didn’t work, I’d nudge the numbers until it did. This time, I decided it is high time I...