CSE 134B Web Client Languages
  1. Presentation
  2. Images
  3. Responsive
  4. Fluid and Responsive Images

Presentation · Images 06

Fluid and Responsive Images

A responsive layout changes shape with the screen, and its images have to change with it: shrink without distorting, hold their place before they arrive, and come in a file no bigger than the space they fill. That last part means telling the browser about a layout it has not seen yet.

Images in HTML (page 04) taught the syntax of srcset, sizes and <picture>. This page is about the reasoning around it: what CSS has to do so an image fits a fluid layout, how the page keeps still while images load, and how to get from a layout in a style sheet to the exact sizes value that describes it. Media and container queries themselves are taught on the CSS section's Responsive design page.

1. The fluid baseline

An <img> is a replaced element: the browser lays out a box for it, and the content of that box comes from outside the document, from the image file. Left alone, the box takes the image's natural size, the pixel width and height stored in the file, as a size in CSS pixels. A 1600-pixel photograph is 1600 CSS pixels wide, whatever column it sits in, and on a phone it pushes the whole page sideways.

Where this started: percentage widths

The early answer to fluid layouts, long before the term "responsive design", was to give the image a percentage in its width attribute, so it would always fill its column:

<img src="lake-1600.jpg" width="100%" alt="Mount Hood reflected in a forest lake.">

It degraded gracefully, in that the image always fit, but it had three faults. A small image in a wide column was stretched past its natural size and blurred, because the browser had to invent pixels. A large image in a narrow column was sent at full weight and scaled down, wasting the bytes. And with no height, the browser had no idea how tall the image would be until it arrived. The attribute is not even valid today: HTML defines width and height on <img> as plain non-negative integers, in CSS pixels, with no percentages.

The modern baseline

The fix moved to CSS, which is where layout decisions belong. Two declarations, applied to every image, make images fluid without making them grow:

@layer base { img, picture, video, svg { max-inline-size: 100%; block-size: auto; } }

max-inline-size: 100% lets the image shrink to fit its container but never stretch past its own size: a 480-pixel image in a 900-pixel column stays 480 pixels wide and sharp. (max-inline-size is the logical name for max-width in a horizontal writing mode; see Layout for logical properties.)

block-size: auto is the half people forget, and it is there because of the width and height attributes. Those attributes are presentational hints: the browser turns them into CSS width and height declarations, treated as author styles with zero specificity that come before every rule you write. Any CSS you write therefore overrides them, which is the improvement the old approach lacked. But the hint for height still applies unless something overrides it. Shrink the width with max-inline-size and the height stays at 533 pixels, and the photograph is squashed. block-size: auto overrides the height hint, so the height follows the width and the proportions hold.

An 800 by 533 image in a narrow column, three waysThree panels, each with a dashed column narrower than the image. In the first, with only the width and height attributes, the image keeps its full width and overflows the column. In the second, with max-inline-size of 100 percent, the image is as wide as the column but keeps its full height, so it is squashed. In the third, with block-size auto added, the image is as wide as the column and its height shrinks in proportion.attributes onlymax-inline-size: 100%+ block-size: autooverflows the columnsquashed: height keptshrinks in proportion
The dashed rectangle is the column. Only the third panel, with both declarations, fits the column and keeps the photograph's shape.
Mount Hood, snow-covered, reflected in a still forest lake at dusk, with dark pines along the right shore.
Narrow the window and the photograph shrinks with it, keeping its shape. Behind it are four widths in three formats; the browser picked one. Photograph: Lake Mountain Landscape, Bonnie Moreland, CC0.

2. Reserving space: width, height and aspect-ratio

Images arrive after the HTML. The browser lays out the text as soon as it can, and an image may follow a moment or several seconds later. If the browser does not know how big the image will be, it gives it no space; when the file arrives, the image takes its space and everything after it moves down. That movement is a layout shift.

The fix is the oldest advice in this section, and for a few years it seemed to stop working. When fluid images became the norm, the CSS overrode the attributes' width and the attributes' height was cancelled by block-size: auto, so in those years writing width and height reserved nothing. Many developers stopped writing them.

Browsers fixed this in 2019 and 2020, and the HTML standard now describes it. The width and height attributes of an <img> map to the CSS aspect-ratio property as well as to width and height. The browser's own style sheet in effect contains:

img { aspect-ratio: auto 800 / 533; }

The auto means "use the image's own ratio once it has loaded; until then, use 800 / 533". So the fluid baseline and the attributes work together. CSS decides the width (the column, at most the image's own width), block-size: auto lets the height follow, and the aspect ratio from the attributes tells the browser what "follow" means before a single byte of the image has arrived. The box is the right shape from the first layout, and the image fills it when it comes.

That is why the section's second image rule says every content image has width and height. The numbers do not have to be the size it will be displayed at; they only have to have the right ratio. The convention is to use the natural size of the fallback file, which is what this site does.

The same page before and after its image arrives, without and with dimensionsTwo rows of two small page sketches. Top row, without width and height: before the file arrives, the text sits at the top of the page; after, the image appears above it and the text has moved down, marked as a shift. Bottom row, with width and height: before the file arrives, an empty box of the right shape sits above the text; after, the image fills that box and the text has not moved.before the file arrivesafter it arrivesno widthor heightshiftwidth andheight setstill
Without dimensions, the browser lays the text out where the image will go, then moves it. With them, the browser reserves a box of the right shape, and the image fills it.

Layout shift as a number: CLS

Shifts are not only ugly. A reader who is halfway through a sentence loses their place; a reader about to tap a link taps an advertisement that slid under their finger instead. Google measures this as Cumulative Layout Shift (CLS), one of the three Core Web Vitals alongside Largest Contentful Paint (LCP), which measures loading, and Interaction to Next Paint (INP), which measures responsiveness. Image performance (page 08) covers LCP.

Each unexpected shift gets a score: the fraction of the viewport the moving content touched, multiplied by the fraction of the viewport it moved. Shifts that come in a burst, less than a second apart and within five seconds overall, form a session window, and a page's CLS is the total of its worst window. Shifts within half a second of a tap, click or key press do not count, because the reader expected something to change. A CLS of 0.1 or less is good, above 0.25 is poor, and web.dev's guidance is to meet the target at the 75th percentile of real page loads, on phones and desktops separately.

When the attributes are not enough: CSS aspect-ratio

The attributes cover images whose size you know. Two cases need the CSS aspect-ratio property, which is Baseline widely available. The first is a box you want at a fixed shape whatever the file's own shape, such as a row of tiles; section 6 builds one. The second is an embedded video or map in an <iframe>, which has no natural size to derive a ratio from:

@layer components { figure iframe { inline-size: 100%; block-size: auto; aspect-ratio: 16 / 9; border: 0; } }

An author aspect-ratio rule beats the one derived from the attributes, for the same reason all author CSS beats presentational hints. Keep the attributes anyway: they are what the browser uses if the style sheet is slow or never arrives.

3. Resolution switching and art direction

A fluid image fits its column; it does not yet send the right file. Responsive images, the markup page 04 teaches, solve two different problems, and it matters which one you have, because they are written differently and the browser treats them differently.

Resolution switchingArt direction
The problemThe same picture is needed at different pixel sizes, for different layouts and densitiesA different picture, usually a different crop, suits a different layout
The filesOne image scaled to several widthsDifferent images, each possibly at several widths
The markup<img srcset sizes><picture> with <source media>
Who decidesThe browser, which may weigh density, network and cacheThe author: the first <source> whose media matches must be used
If it goes wrongA file a little too large or too softThe wrong composition: a mountain too small to see on a phone

Most images need only resolution switching. The browser knows the screen, the zoom level and sometimes the connection, and the author knows the layout. The srcset attribute is a list of image candidate strings, each a URL followed by a width descriptor such as 800w, meaning "this file is 800 pixels wide". The sizes attribute says how wide the image will be laid out. From those, the browser works out the pixel density each candidate would give and picks one. Because the decision is the browser's, it is allowed to choose a smaller file to save data, or reuse a larger one it already has in its cache.

Art direction is the exception, used when scaling is not enough. The lake photograph is a wide landscape; at phone width the mountain becomes a small white triangle. A square crop around the peak tells the story better on a narrow screen:

Close crop of the same scene: the snow-covered peak of Mount Hood and its reflection in the lake.
The square crop centered on the peak, made by the same build script from the same photograph. An art-directed <picture> would use it below some width and the landscape above it. This figure is lazy-loaded with sizes="auto", the subject of section 5.

Note what art direction costs: every crop is another set of files at several widths and in several formats, and a <source> for each combination. Use it when the composition really fails at small sizes, and use resolution switching for everything else. The same <picture> mechanism, with type instead of media, chooses formats; page 04 shows both, and its art-direction demo puts the two crops on screen.

4. A worked example of sizes, from layout to attribute

The sizes attribute is where responsive images go wrong most often, because it asks the author to describe, in a small language of its own, a layout that lives in the style sheet. The method is mechanical: read the layout, write down the image's width in each of its states, and translate. Here it is for a common layout, a grid of cards that each start with a photograph.

The layout

@layer layout { :root { --gutter: 1rem; --gap: 1.5rem; --max: 70rem; } main { box-sizing: border-box; max-inline-size: var(--max); margin-inline: auto; padding-inline: var(--gutter); > ul { display: grid; gap: var(--gap); padding: 0; list-style: none; @media (width >= 40rem) { grid-template-columns: repeat(2, minmax(0, 1fr)); } @media (width >= 64rem) { grid-template-columns: repeat(3, minmax(0, 1fr)); } } } }

Each card is an <li> whose image fills its width. Read the layout as a function of the viewport width, written here as 100vw:

  1. Under 40rem, one column. The image is the viewport minus the two gutters: 100vw - 2rem.
  2. From 40rem to 64rem, two columns. Take away the gutters and the one gap, then halve: (100vw − 2rem − 1.5rem) / 2, which is 50vw - 1.75rem.
  3. From 64rem to 70rem, three columns, still growing. Take away the gutters and two gaps, then divide by three: (100vw − 2rem − 3rem) / 3, about 33.33vw - 1.67rem.
  4. From 70rem up, the container stops growing. The content box is 70rem − 2rem = 68rem; less two gaps leaves 65rem; a third of that is a fixed 21.67rem, about 347 CSS pixels, however wide the window gets.
The card grid's image width in three of its statesThree viewport sketches, not to scale. Under 40rem, one card fills the viewport between the gutters, and its image width is 100vw minus 2rem. From 40 to 64rem, two cards share the width, each 50vw minus 1.75rem. From 70rem up, the grid is capped at 70rem in the middle of a wider viewport, and each of three cards is a fixed 21.67rem, about 347 CSS pixels.under 40remone column100vw - 2rem40rem to 64remtwo columns50vw - 1.75rem each70rem and upthree columns, capped21.67rem each (about 347 px)
Not to scale. Solid outline: the viewport; dashed: the grid at its 70rem maximum; gold: each image. The fourth state, three columns between 64rem and 70rem, is the same as the third without the cap.

The attribute

Each state becomes one source size: a media condition and a length. The browser reads them in order and uses the first whose condition is true, so the widest condition goes first, and the last entry, with no condition, is the default:

<li> <img src="lake-800.jpg" srcset="lake-480.jpg 480w, lake-800.jpg 800w, lake-1200.jpg 1200w, lake-1600.jpg 1600w" sizes="(min-width: 70rem) 21.67rem, (min-width: 64rem) calc(33.33vw - 1.67rem), (min-width: 40rem) calc(50vw - 1.75rem), calc(100vw - 2rem)" alt="Mount Hood reflected in a forest lake at dusk." width="800" height="533" loading="lazy"> <h2>Lake loop</h2> … </li>

Three details in that value are easy to get wrong. The lengths can be calc() expressions but not percentages, because a percentage of what? The browser has not laid out the page. A rem in sizes, as in any media condition, means the browser's default font size, usually 16 pixels, not whatever the page's style sheet sets on html; that is why the layout here uses rems and leaves the root font size alone. And 100vw includes the scrollbar where scrollbars take up space, so the value slightly overestimates the slot on desktops. That is the safe direction: a candidate a few percent too large, never too small.

What the browser picks

With the slot width known, the browser divides each candidate's width by it to get the density that candidate would deliver, and compares that with the device pixel ratio. The HTML standard leaves the final choice to the browser, but in practice browsers take the smallest candidate that meets the DPR. The table works it through for the AVIF versions of the four lake files (the <picture> in section 1 offers exactly these):

Which lake file the card grid gets, by viewport and DPR (the site's AVIF encodes)
Viewport (CSS px)DPRSlot (CSS px)Pixels neededChosen fileBytes
3752343686lake-800.avif21,823
37533431,029lake-1200.avif45,095
60035681,704lake-1600.avif74,761
8001372372lake-480.avif8,760
8002372744lake-800.avif21,823
10241315315lake-480.avif8,760
10242315629lake-800.avif21,823
14401347347lake-480.avif8,760
14402347693lake-800.avif21,823
19202347693lake-800.avif21,823

The pattern is worth noticing. The largest files go to the phones, because a single-column card on a dense screen needs more pixels than a third-of-a-row card on a desktop. At 600 pixels and DPR 3 no candidate is big enough, so the browser takes the largest, 1600, and scales it up slightly. And on a wide desktop the image never changes, however wide the window, because the grid stopped growing at 70rem.

Now delete the sizes attribute. With width descriptors and no sizes, the browser assumes 100vw: the image fills the window. On the 1440-pixel desktop at DPR 1 it wants 1,440 pixels and downloads lake-1600.avif, 74,761 bytes, instead of 8,760: more than eight times the data for a card that is 347 pixels wide. That is the cost of a missing or careless sizes, multiplied by every card on the page.

Why sizes can only see the viewport

The grid above changes columns with media queries, so sizes could copy them. Change the grid to a container query, or put the same card component in a narrow sidebar and a wide main column, and sizes has no way to say so. Its media conditions are about the viewport, and there is no container condition in sizes.

The reason is timing, and it is the whole point of responsive images. The browser chooses a candidate while it is still parsing the HTML, often in the preload scanner that runs ahead of the parser looking for files to fetch, before the style sheets have arrived and long before any layout exists. At that moment the only facts it has are the viewport size and the DPR. The author's sizes value is a promise about a layout the browser has not computed yet. A container's width is a result of layout, so it is not available to that early choice.

In practice there are three answers. A component that sits in containers whose widths are themselves a function of the viewport can still get a viewport sizes: work out the container's width at each breakpoint, as above. A component used in many places can get a sizes written for its widest likely slot, accepting some waste in narrow places. And an image that is lazy-loaded can let the browser measure it, which is the next section.

5. sizes="auto" with lazy loading

A lazy-loaded image is not fetched until the reader scrolls near it, by which time the page has been laid out and the image's box already has a width. For those images the timing argument above does not apply, so the HTML standard added a keyword that tells the browser to use that width:

<img src="lake-800.jpg" srcset="lake-480.jpg 480w, lake-800.jpg 800w, lake-1200.jpg 1200w, lake-1600.jpg 1600w" sizes="auto, (min-width: 40rem) calc(50vw - 1.75rem), calc(100vw - 2rem)" alt="Mount Hood reflected in a forest lake at dusk." width="800" height="533" loading="lazy">

The rules are strict. auto works only on an image with loading="lazy", and only as the first entry in sizes. When it applies, the browser uses the width of the image's laid-out box, so a container query, a sidebar or a flex item all just work. The entries after auto are a fallback: a browser that does not support the keyword skips it as an invalid entry and uses the rest, so always write a real sizes after it.

It also needs the image's dimensions. The box has to have its real width before the file arrives, which is what width, height and the fluid baseline provide. The standard warns that without dimensions the image will probably be laid out at 300 by 150 pixels, the default size for a replaced element with no other information, and the browser will choose a candidate for a 300-pixel box.

Support for sizes="auto", October 2026
EngineSince
Chromium (Chrome, Edge, Opera, Samsung Internet)Chrome 126, June 2024
Gecko (Firefox)Firefox 150, April 2026
WebKit (Safari)In the Safari 27 betas, announced at WWDC 2026; Safari 26 and earlier do not support it

So sizes="auto" is arriving in every engine, but it is not yet Baseline, and the Safari versions still in wide use ignore it. That is harmless as long as the fallback is good: those browsers use the rest of the attribute exactly as they would have without auto. The keyword is a progressive enhancement that costs nothing to write.

6. Images in grid, flex and container layouts

The fluid baseline behaves well in normal flow. Grid and flex layouts add two surprises, and tiled galleries add a third need.

Tracks and items that will not shrink

A grid track sized 1fr has a minimum of auto, which means it will not shrink below the smallest size its contents can take. An image with the fluid baseline is not a problem here: a percentage maximum lets an image shrink to nothing, so it never forces a track wider. But an image without the baseline forces the track to the image's full natural width, and so does anything else that cannot shrink, such as a wide <pre> or a long URL in the caption beside it. The column overflows and takes the page with it.

Flex items have the same minimum, and a second habit: an item's starting size, its flex basis, is by default the size of its content. A figure holding a 1600-pixel photograph starts at 1600 pixels and shrinks only until its neighbor reaches its own minimum, so the photograph takes nearly the whole row and squeezes the text beside it into a sliver.

@layer layout { main > ul { display: grid; /* Each track at least 16rem (or the whole row, if narrower), then a share of the rest. An explicit minimum replaces the automatic one. */ grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 1.5rem; > li { min-inline-size: 0; } } article > header { display: flex; gap: 1rem; > figure { flex: 1 1 0; min-inline-size: 0; } > hgroup { flex: 2 1 0; } } }

min-inline-size: 0 on a grid or flex item, or minmax(0, 1fr) for a grid track, removes the automatic minimum, so the item can be narrower than its content and the content's own rules (the fluid baseline, overflow-wrap, a scrolling <pre>) take over. A flex basis of 0 makes the items share the row by their grow factors instead of by their content. The card grid in section 4 used minmax(0, 1fr) for this reason. The CSS section's Layout page covers the sizing rules behind both.

Uniform tiles: aspect-ratio and object-fit

Photographs come in many shapes, and a gallery of them in a grid looks ragged. The fix is to decide the tile's shape in CSS and let each photograph fill it, cropping what does not fit:

@layer components { main > ul img { inline-size: 100%; aspect-ratio: 4 / 3; object-fit: cover; object-position: 50% 40%; } }

The author aspect-ratio overrides the one from the attributes, so every tile is 4 by 3, and the space is still reserved before any file arrives. object-fit: cover scales each image to cover its box and crops the overflow, the way background-size: cover does for a background; object-position chooses which part survives the crop. Without object-fit, a portrait photograph forced into a landscape box is squashed. Images in CSS (page 05) covers both properties in full.

Cropping is a decision about content. object-fit: cover crops blindly from the position you give, so a face near the edge of a portrait can be cut off in a landscape tile. When the crop matters, make it on purpose, as the lake-peak files were, and serve it with art direction.

Containers

Container queries let a card change its own layout, image above text in a narrow slot and beside it in a wide one, whatever the viewport is. The image's CSS follows without any help: it is fluid, so it takes whatever width its part of the card gets. Only the choice of file has to be thought about, and section 4 and section 5 between them give the answer: a sizes value worked out from the viewport where the containers' widths follow the viewport, a generous one where they do not, and sizes="auto" on every lazy image so browsers that support it can measure instead of guess.

7. Summary

ConceptKey points
The fluid baselinemax-inline-size: 100% lets images shrink but not stretch; block-size: auto overrides the height hint from the attributes so they keep their shape. Percentage width attributes are history, and invalid.
Reserving spacewidth and height map to aspect-ratio: auto w / h, so the box has the right shape before the file arrives. Use CSS aspect-ratio for fixed shapes and embeds.
CLSA Core Web Vital: the worst burst of unexpected shifts, each scored as impact times distance. Good is 0.1 or less at the 75th percentile. Lazy images without dimensions are a common cause.
Switching or directingResolution switching sends one picture at the right size, with srcset and sizes, and the browser decides. Art direction sends a different crop with <picture> and media, and the author decides.
Writing sizesRead the layout, write the image width in each state, list the widest condition first and a default last. Use calc(), no percentages; a rem is the default font size. Without sizes, the browser assumes 100vw.
The viewport limitThe browser picks a file before CSS and layout exist, so sizes can see only the viewport, never a container.
sizes="auto"For lazy images only, first in the list, with a real fallback after it and dimensions on the image. In Chromium and Firefox; arriving in Safari 27; not yet Baseline.
Grid and flexmin-inline-size: 0 or minmax(0, 1fr) lets items shrink; a flex basis of 0 shares the row. aspect-ratio with object-fit: cover makes uniform tiles.