ericportis.com

Sizes="auto" pretty much requires width and height attributes

Writing, reading, and maintaining sizes attributes is the worst part about authoring responsive images; auto-sizes for lazy-loaded images is a fantastic addition to the platform.

(It is very funny that the Chrome Platform Status entry for sizes=auto currently describes web developers’ collective opinion about the feature as “no signals.” One-in-ten sizes attributes on the web are already using auto, via lazysizes.js! But I digress.)

Auto-sizes just shipped behind the Experimental Web Platform Features flag in Chrome Canary. Which makes this a good time to try to explain the one weird thing about it. In short: in addition to requiring loading=lazy, sizes=auto also basically requires that <img> elements have width and height attributes.

But don’t take my word for it. Here’s a note from Section 4.8.2.2 of the HTML specification, Sizes attributes:

Note: […] it is strongly encouraged to specify dimensions using the width and height attributes or with CSS. Without specified dimensions, the image will likely render with 300x150 dimensions because sizes="auto" implies contain-intrinsic-size: 300px 150px

What!? 300×150? It’s true! Here’s an <img> element that uses sizes=auto:

<img
	loading="lazy"
	sizes="auto"
	srcset="
		https://o.img.rodeo/w_1200,h_1200,b_tomato/t_WxH/_.png   1200w,
		https://o.img.rodeo/w_900,h_900,b_goldenrod/t_WxH/_.png   900w,
		https://o.img.rodeo/w_600,h_600,b_forestgreen/t_WxH/_.png 600w,
		https://o.img.rodeo/w_300,h_300,b_dodgerblue/t_WxH/_.png  300w"
	src="https://o.img.rodeo/w_300,h_300,b_forestgreen/t_WxH/_.png"
	alt="An example image that reports its natural dimensions"
>

And here’s what that looks like right now, in Canary, with the Experimental Web Platform Features flag on:

Screenshot of a Chrome window, showing a Codepen titled “sizes=auto, without width and height”. It’s a mostly blank page, showing a small green rectangle (the img), which has some squished-looking text reading “600x600” in it.
Note that Canary did load the best resource to fit (300px@2x = 600 device pixels).

Before I answer some obvious questions (what is happening, why), let’s tackle the practical: how do I fix it?

What values should I use for height and width?

You should use the height and width, in pixels, of the largest resource that your <img> can serve. If you’re using srcset, that means the dimensions of the largest resource in your srcset:

<img
	loading="lazy"
	sizes="auto"
	width="1200"
	height="1200"
	srcset="
		https://o.img.rodeo/w_1200,h_1200,b_tomato/t_WxH/_.png   1200w,
		https://o.img.rodeo/w_900,h_900,b_goldenrod/t_WxH/_.png   900w,
		https://o.img.rodeo/w_600,h_600,b_forestgreen/t_WxH/_.png 600w,
		https://o.img.rodeo/w_300,h_300,b_dodgerblue/t_WxH/_.png  300w"
	src="https://o.img.rodeo/w_300,h_300,b_dodgerblue/t_WxH/_.png"
	alt="An example image that reports its natural dimensions"
>

(If you’re using Client Hints, you should use the dimensions of the full-size resource, before any server-side downscaling for delivery. Here’s a (Chrome-only) example that uses Client Hints.)

Depending on what else you have going on in your CSS, adding width and height to your HTML might unexpectedly distort your <img>, giving it a garish, fixed height which will pair terribly with its delightfully flexible width. One line of CSS will fix that for you:

img {
	height: auto; 
}

All together, now:

<style>
img {
	width: 100%;
	height: auto;
}
</style>

<img
	loading="lazy"
	sizes="auto"
	width="1200"
	height="1200"
	srcset="
		https://o.img.rodeo/w_1200,h_1200,b_tomato/t_WxH/_.png   1200w,
		https://o.img.rodeo/w_900,h_900,b_goldenrod/t_WxH/_.png   900w,
		https://o.img.rodeo/w_600,h_600,b_forestgreen/t_WxH/_.png 600w,
		https://o.img.rodeo/w_300,h_300,b_dodgerblue/t_WxH/_.png  300w"
	src="https://o.img.rodeo/w_300,h_300,b_dodgerblue/t_WxH/_.png"
	alt="An example image that reports its natural dimensions"
>

And here’s a screenshot of what that looks like in Canary with the flag on:

Screenshot of a Chrome window, showing a different Codepen, titled “sizes=auto, with width, height, and…” (unfortunately the title is truncated!?). Anyways in this one the img is gold/yellow, square, and fills almost the whole viewport. Inside if it is not-squished, normal-looking text, reading “900x900”.
All better!

Perhaps you’re already adding height and width to your responsive image HTML and height: auto to your CSS in order to prevent layout shifts. Great! Change nothing!

Ok, so:

What is happening and why?

By default, images on the web are rendered at their natural (aka intrinsic) dimensions. If you stick a 300×300 resource into an <img> and do nothing else, its layout size will be 300×300.

The natural dimensions of responsive images get surprisingly weird, fast, but:

  1. There are a number of ways that a loaded resource’s natural dimensions can affect the layout dimensions of its <img>.
  2. The value of sizes=auto updates whenever an <img>’s layout dimensions change, possibly triggering a new resource load.
  3. A new resource will have new natural dimensions, which can affect the <img>’s layout dimensions, which will update the sizes=auto value, which may trigger a new resource load; the new resource will have new natural dimensions, which …&c.
An illustration of the endless cycle. Three items are arranged in a circle. The top, centered one says “measured layout width”. There’s an arrow pointing from it to the second item in the bottom right, “sizes=auto”. Then there’s an arrow pointing from that to the third item, in the bottom left, “resource selection”. This one has three little icons signifying the different sizes of available images in light gray above it. Lastly, there’s another arrow, from “resource selection” back to “measured layout width” at the top.
A lot of the spec discussion around sizes=auto was just: people coming up with corner cases that would load every single resource in a srcset, one-after-the-other, using this mechanism.

Simon Pieters, who wrote the auto-sizes spec, made a few valiant and clever attempts to define all of the cases that could possibly trigger cyclical dependencies and work around them in ways that wouldn’t be too weird or noticeable for authors. But when the rubber hit the road and implementers started implementing, those workarounds didn’t hold up. Ultimately, ’twas decided that the only way forward was to make a clean cut: mandate that a loaded resource’s natural dimensions cannot affect its <img>’s layout in any way, ever, when sizes=auto is involved. Luckily, we have a fairly new feature that explicitly does exactly that: size containment.

The exact same diagram as before, except some red scissors have appeared, which are cutting the arrow between “resource selection” to “measured layout width”.
Snip snip!

So now, this is in Chrome Canary’s UA stylesheet:

img:is([sizes="auto" i], [sizes^="auto," i]) {
    contain: size !important;
    contain-intrinsic-size: 300px 150px;
}

That says: are we dealing with an <img> with sizes=auto? Then its natural dimensions are 300×150. NO EXCEPTIONS.

300×150 was chosen because that’s what <video> and <canvas> do; all of these elements use this smallish-but-non-zero default size in order to encourage you to do better.

Finally, we get to the load-bearing weasel-words in the title of this post: “sizes=auto pretty much requires width and height attributes”. width and height are not the only way! I think the pattern that I’m pushing, where you describe the aspect ratio of your content in HTML, and give it a flexible size in CSS – provides a better separation of concerns than any other solution. But the fundamental takeaway is: when you use sizes=auto, your image will be assigned a natural size of 300×150 and a natural aspect ratio of 2:1, and it’s on you to override those dimensions.