D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

Image Block Renders 528px Wide in a 335px Column After a Landscape Swap? `min-height` Makes `aspect-ratio` Compute Width, Not Height

· · 6 min read
Image Block Renders 528px Wide in a 335px Column After a Landscape Swap? `min-height` Makes `aspect-ratio` Compute Width, Not Height

The only thing I changed that day was one image file. Not the CSS, not the theme template, not the build. I swapped the photo in an image block from portrait to landscape because the art direction called for it, and the page immediately looked wrong: the image box hung far outside its grid cell, ran into the column beside it, and then got sliced off cleanly at the section edge.

I measured the box. It was 528px wide, inside a column that is 335px wide. That is 193px hanging past the boundary that was supposed to contain it. The CSS governing that block had been sitting there for close to a year without ever causing trouble.

That was not the part that ate my afternoon, though. The responsive sweep I run every time I touch layout came back green. document.documentElement.scrollWidth matched clientWidth exactly, the page would not budge sideways by a single pixel, and the report said zero horizontal overflow. So I had a screenshot that was visibly broken on one side and a measurement telling me everything was fine on the other.

The sweep was not wrong, it was answering a different question

One of the block's ancestors carried overflow-x: clip. That is the whole explanation. The extra width is absorbed there before the document ever gets a chance to grow, so scrollWidth genuinely never increases and the page genuinely never scrolls sideways. The defect does not go away, it just becomes invisible to the one metric I was trusting.

This was not the first time the same site caught me with it. Earlier, a sweep at 320px had reported zero overflow across many sessions in a row, right up until I measured elements against their own containers instead of against the document. Five partner names in a collaborations card list turned out to be pushing 24 to 59px past their individual cards, and two of them were already being cut mid word by the section's overflow-x: clip. Same lesson, and clearly I had not swallowed it yet: a green metric only tells you about the one pair of boxes it compares.

Document overflow and element overflow are two separate checks. If you only run the first one, every ancestor with overflow-x: clip or hidden is a blind spot you agreed to yourself.

The culprit: a min-height sitting on an element that has aspect-ratio

Once I started trusting my eyes over the sweep, I opened the block's computed styles. Two declarations that look unrelated:

.image-block {
  aspect-ratio: var(--file-ratio);
  min-height: 320px;
}

Each one is reasonable on its own. aspect-ratio reserves a box that matches the file, and min-height was added long ago so the placeholder would not collapse into a hairline before the image loaded. Put together, they become something else.

aspect-ratio derives the indefinite dimension from the definite one. min-height makes the height definite. So the direction of the calculation flips: the ratio is no longer computing height from the column's width, it is computing width from that 320px of height. From that moment on, min-height stopped being a height rule and became a width declaration in disguise.

Which dimension actually binds is then decided entirely by the ratio of the photo file:

The 528 is not a coincidence and it is not layout getting confused. It is a correct multiplication performed by an incorrect rule. The CSS did precisely what it said. What it said was just not what anyone meant.

The fix is deletion, not tuning

The obvious temptation is to lower min-height until the landscape case fits again. I did not, because that only moves the threshold. As long as the height stays definite, the width keeps being derived from it, and the next file with a slightly wider ratio blows out in exactly the same way. The thing that needs removing is not the number, it is the direction of the calculation.

So the min-height came out. Its original reason no longer applies: aspect-ratio already reserves the box by itself, so the placeholder will not collapse even before the image loads. Once a ratio exists, a min-height that was added as a collapse guard is almost always redundant.

The working rule I use now: if an element has aspect-ratio, treat any min-height or height on it as a width declaration. Re-read it every time the art direction flips orientation, because the file's orientation is the input that decides which side binds.

Verify by comparing an element against its own container

Since the document sweep is provably blind to this class of defect, I changed what gets measured. For each candidate, take the difference between its right edge and its container's:

function rightOverhang(el, container) {
  return Math.round(
    el.getBoundingClientRect().right - container.getBoundingClientRect().right
  );
}

Anything above zero means the element has left the box that was supposed to contain it. If there is a clipping ancestor above it, that number will never show up as a scrollbar. It shows up as content that is cut.

Clipping ancestors are worth hunting for first, since they mark exactly where the document check loses its sight:

let n = el.parentElement;
while (n) {
  const ox = getComputedStyle(n).overflowX;
  if (ox === 'clip' || ox === 'hidden') console.log(n, ox);
  n = n.parentElement;
}

One detail kept this hidden from me longer than it should have: images have no text. A sweep that walks text nodes looking for clipped lines will skip an image block entirely, no matter how clean the resulting report reads.

A rule that slept for a year is not a rule that was right

That min-height: 320px was wrong from the day it was written. It had simply never met a file that triggered it. For a year every photo that came in happened to be portrait, and every one of those made the rule produce a width smaller than the column, so there was never any evidence that anything was off. Quiet code is not verified code. Sometimes it is just code that has not met its data yet.

Two things came out of this, and both are about what we consider already checked. First, aspect-ratio works in both directions, so any height declaration attached to it is quietly a width declaration too. Second, one green sweep does not close a whole class of defect, and an overflow-x: clip anywhere in the ancestor chain is standing permission to fail silently. After getting caught twice on the same site, I stopped asking whether the page scrolls sideways and started asking whether every box still fits inside its parent.