D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

Swapped the Image Files, Touched No CSS, and They Rendered Roughly Twice as Big? `max-width` Constrains the Canvas, Not the Subject

· · 6 min read
Swapped the Image Files, Touched No CSS, and They Rendered Roughly Twice as Big? `max-width` Constrains the Canvas, Not the Subject

The client sent three new packaging design PNGs. I converted them to WebP with cwebp -lossless -z 9 -metadata none, which landed at 178 to 187KB per file, then dropped them over the old files under the exact same names. Not a single line of code needed touching. The old WebPs went into a separate backup folder first, with a -prev suffix on the filename, so the previous version could be restored if it came to that.

The client opened the page and the reply was two words: too big.

Not a colour problem, not a crop problem. Just the size, and the gap was roughly double. That figure is an eyeball estimate, not a pixel measurement.

The thing that did not change is the measuring stick

The rule constraining those images was never touched between the old asset and the new one:

max-width: 320px;
height: auto;

The reflex is to chase whatever changed, and here the only thing that changed was file content. Precisely because the constraining box stayed constant at 320px, it became the one thing at the scene I could trust as a baseline.

From there the logic more or less forces itself. If the constraint is still 320px while what the client sees is roughly twice as large, then that constraint is not what decides perceived size. What changed is the relationship between the subject and the canvas holding it, and that relationship does not live in CSS. It lives inside the image file.

Read the dimensions first

Before guessing anything about the contents, read the numbers. ffprobe is enough for this, and its output is readable at a glance:

ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height -of csv=s=x:p=0 old.webp

The old file was 2400x2400, and its subject occupied only about 50% of the canvas width because of the padding around it. The new file was 1195x1600, a tight crop, with the subject filling the frame.

The root cause: CSS constrains the canvas, not the subject

max-width: 320px acts on the image element, which means on the file's entire canvas, not on the object drawn inside it. The browser has no idea which pixels are subject and which are empty room. To it they are all pixels that scale together until the element is 320px wide.

So the arithmetic goes like this. In the old file, a 2400 pixel canvas scales down to fit 320px, and because the subject only takes up around 50% of the canvas width, what you actually see is about half of those 320px. The rest is empty room eating into the same width budget. In the new file, a 1195 pixel canvas also scales down to 320px, but this time the subject fills its canvas, so what you see lands close to the full 320px.

The CSS numbers were identical. The rendered size was roughly double. As a bonus, because of height: auto, the element's height follows the file's ratio, so moving from a square canvas to a portrait one shifted the block height too while the stylesheet sat perfectly still.

The general rule can be written out. When you drop-in replace an image asset under the same filename, if the new file's canvas dimensions differ from the old one, or its subject to canvas ratio differs, the rendered size changes even though the CSS was never touched. The only genuinely safe case is when the canvas dimensions match and the subject to canvas proportion matches, not one or the other.

Why the fix did not come from CSS

The fastest route out is obviously to retune max-width until it looks right. The note I left from this case argues against exactly that: patching from the CSS side is riskier, because it can break the image's alignment with its siblings in flex or grid layouts.

There is also more than one number involved. That image's width is responsive per breakpoint, at 70vw, 50vw and 36vw, so the compensation is not a single value typed once.

The decision was to prefer the asset side fix over the CSS side fix, so that visual continuity stays one to one.

The fix: restore the old canvas

All it needed was to give the new file the same empty room the old one had, with the subject centred. The pad filter in ffmpeg does exactly that without touching a single subject pixel, it simply attaches extra area around the original image.

The padding is done on the client's PNG rather than directly on the WebP, and the result is converted again afterwards:

ffmpeg -i new.png \
  -vf "pad=2400:2400:(ow-iw)/2:(oh-ih)/2:color=0x00000000" \
  -y padded.png

The 2400:2400 is not a round number I invented, it is the old file's dimensions that ffprobe just reported. The (ow-iw)/2 and (oh-ih)/2 expressions place the original image dead centre in its new canvas, and color=0x00000000 is black at zero alpha, so the added area is genuinely transparent.

Only then does it get encoded back to WebP, with the same flags as the first conversion:

cwebp -lossless -z 9 -metadata none padded.png -o new.webp

The working order really is those three steps: ffprobe to read the dimensions, ffmpeg pad to match the canvas, then cwebp.

The result was about 190KB per file, dimensions matching the old asset, and the rendered size back to what it was.

The same symptom once showed up in reverse

A month earlier, a logo on the same project gave the mirror image of this. The old logo was 1304x1310, square, mostly white padding, with the text sitting in the lower middle. Displaying it required a workaround: an overflow: hidden viewport, the image scaled up to a height of 280, then nudged into place with a transform: translate.

The new logo arrived on a 2400x913 canvas with minimal padding, its text filling roughly 85% of the width and 50% of the height. Because the subject to canvas ratio had changed so drastically, the workaround could simply be dropped, and the logo rendered naturally with height: 36px; width: auto; max-width: none.

Same law, opposite direction. A padded canvas forces tricks into the CSS, a tight canvas makes those tricks unnecessary.

The same rule came back for video

The rule got applied again later, when a new version of a video asset came in. As delivered, its subject occupied 30.8% of the frame width against 24.9% for the old asset already in place, so dropped in as-is it would have rendered 24% bigger. It was matched to the old asset with scale=988:1560 inside a 1440x1920 canvas, landing the ratio exactly on 24.9%.

The lesson I wrote down there: compare a new asset against the asset that was already accepted, not against the new file's own internal baseline.

What I took away