A Guide to the Responsive Images Syntax in HTML

Avatar of Chris Coyier
Chris Coyier on (Updated on )

This guide is about the HTML syntax for responsive images (and a little bit of CSS for good measure). We’ll go over srcset and , plus a whole bunch of things to consider to help you get the best performance and design control from your images.

This guide is about the HTML syntax for responsive images (and a little bit of CSS for good measure). The responsive images syntax is about serving one image from multiple options based on rules and circumstances. There are two forms of responsive images, and they’re for two different things:

If your only goal is…

Increased Performance

Then what you need is…

<img
  srcset=""
  src=""
  alt=""
>

There is a lot of performance gain to be had by using responsive images. Image weight has a huge impact on pages’ overall performance, and responsive images are one of the best things that you can do to cut image weight. Imagine the browser being able to choose between a 300×300 image or a 600×600. If the browser only needs the 300×300, that’s potentially a 4× bytes-over-the-wire savings! Savings generally go up as the display resolution and viewport size go down; on the smallest screens, a couple of case studies have shown byte savings of 70–90%.

If you also need…

Design Control

Then what you need is…

<picture>
  <source srcset="" media="">
  <source srcset="" media="">
  <img src="" alt="">
</picture>

Another perfectly legit goal with responsive images is not just to serve different sizes of the same image, but to serve different images. For example, cropping an image differently depending on the size of the screen and differences in the layout. This is referred to as “art direction.”

The <picture> element is also used for fallback image types and any other sort of media query switching (e.g. different images for dark mode). You get greater control of what browsers display.


There is a lot to talk about here, so let’s go through both syntaxes, all of the related attributes and values, and talk about a few related subjects along the way, like tooling and browsers.

Table of Contents


Using srcset

The <img srcset="" src="" alt=""> syntax is for serving differently-sized versions of the same image. You could try to serve entirely different images using this syntax, but browsers assume that everything in a srcset is visually-identical and will choose whichever size they think is best, in impossible-for-you-to-predict ways. So I wouldn’t recommend it.

Perhaps the easiest-possible responsive images syntax is adding a srcset attribute with x descriptors on the images to label them for use on displays with different pixel-densities.

<img 
  alt="A baby smiling with a yellow headband."
  src="baby-lowres.jpg"
  srcset="baby-highres.jpg 2x"
>

Here, we’ve made the default (the src) the “low res” (1×) copy of the image. Defaulting to the smallest/fastest resources is usually the smart choice. We also provide a 2× version. If the browser knows it is on a higher pixel-density display (the 2x part), it will use that image instead.

Demo
<img 
  alt="A baby smiling with a yellow headband."
  src="baby-lowres.jpg"
  srcset="
    baby-high-1.jpg 1.5x,
    baby-high-2.jpg 2x,
    baby-high-3.jpg 3x,
    baby-high-4.jpg 4x,
    baby-high-5.jpg 100x
  "
>

You can do as many pixel-density variants as you like.

While this is cool and useful, x descriptors only account for a small percentage of responsive images usage. Why? They only let browsers adapt based on one thing: display pixel-density. A lot of times, though, our responsive images are on responsive layouts, and the image’s layout size is shrinking and stretching right along with the viewport. In those situations, the browser needs to make decisions based on two things: the pixel-density of the screen, and the layout size of the image. That’s where w descriptors and the sizes attribute come in, which we’ll look at in the next section.

Using srcset / w + sizes

This is the good stuff. This accounts for around 85% of responsive images usage on the web. We’re still serving the same image at multiple sizes, only we’re giving the browser more information so that it can adapt based on both pixel-density and layout size.

<img 
  alt="A baby smiling with a yellow headband."
  srcset="
    baby-s.jpg  300w,
    baby-m.jpg  600w,
    baby-l.jpg  1200w,
    baby-xl.jpg 2000w
  "
  sizes="70vmin"
>

We’re still providing multiple copies of the same image and letting the browser pick the most appropriate one. But instead of labeling them with a pixel density (x) we’re labelling them with their resource width, using w descriptors. So if baby-s.jpg is 300×450, we label it as 300w.

Using srcset with width (w) descriptors like this means that it will need to be paired with the sizes attribute so that the browser will know how large of a space the image will be displaying in. Without this information, browsers can’t make smart choices.

Demo

Creating accurate sizes

Creating sizes attributes can get tricky. The sizes attribute describes the width that the image will display within the layout of your specific site, meaning it is closely tied to your CSS. The width that images render at is layout-dependent rather than just viewport dependent!

Let’s take a look at a fairly simple layout with three breakpoints. Here’s a video demonstrating this:

Demo

The breakpoints are expressed with media queries in CSS:

body {
  margin: 2rem;
  font: 500 125% system-ui, sans-serif;
}
.page-wrap {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr 200px;
  grid-template-areas:
    "header header"
    "main aside"
    "footer footer";
}

@media (max-width: 700px) {
  .page-wrap {
    grid-template-columns: 100%;
    grid-template-areas:
      "header"
      "main"
      "aside"
      "footer";
  }
}
@media (max-width: 500px) {
  body {
    margin: 0;
  }
}

The image is sized differently at each breakpoint. Here’s a breakdown of all of the bits and pieces that affect the image’s layout width at the largest breakpoint (when the viewport is wider than 700px):

The image is as wide as 100vw minus all that explicitly sized margin, padding, column widths, and gap.
  • At the largest size: there is 9rem of explicit spacing, so the image is calc(100vw - 9rem - 200px) wide. If that column used a fr unit instead of 200px, we’d kinda be screwed here.
  • At the medium size: the sidebar is dropped below, so there is less spacing to consider. Still, we can do calc(100vw - 6rem) to account for the margins and padding.
  • At the smallest size: the body margin is removed, so just calc(100vw - 2rem) will do the trick.

Phew! To be honest, I found that a little challenging to think out, and made a bunch of mistakes as I was creating this. In the end, I had this:

<img 
  ...  
  sizes="
    (max-width: 500px) calc(100vw - 2rem), 
    (max-width: 700px) calc(100vw - 6rem),
    calc(100vw - 9rem - 200px)
  "
/>

A sizes attribute that gives the browser the width of the image across all three breakpoints, factoring in the layout grid, and all of the surrounding gap, margin, and padding that end up impacting the image’s width.

Now wait! Drumroll! 🥁🥁🥁That’s still wrong. I don’t understand why exactly, because to me that looks like it 100% describes what is happening in the CSS layout. But it’s wrong because Martin Auswöger’s RespImageLint says so. Running that tool over the isolated demo reports no problems except the fact that the sizes attribute is wrong for some viewport sizes, and should be:

<img
  ...
  sizes="
    (min-width: 2420px) 2000px, 
    (min-width: 720px) calc(94.76vw - 274px), 
    (min-width: 520px) calc(100vw - 96px), 
    calc(100vw - 32px)
  "
>

I don’t know how that’s calculated and it’s entirely unmaintainable by hand, but, it’s accurate. Martin’s tool programmatically resizes the page a bunch and writes out a sizes attribute that describes the actual, observed width of the image over a wide range of viewport sizes. It’s computers, doing math, so it’s right. So, if you want a super-accurate sizes attribute, I’d recommend just putting a wrong one on at first, running this tool, and copying out the correct one.

For an even deeper dive into all this, check out Eric Portis’ w descriptors and sizes: Under the hood.

Being more chill about sizes

Another option is use the Horseshoes & Hand Grenades Method™ of sizes (or, in other words, close counts). This comes highly suggested.

For example, sizes="96vw" says, “This image is going to be pretty big on the page — almost the full width — but there will always be a little padding around the edges, so not quite. Or sizes="(min-width: 1000px) 33vw, 96vw" says, “This image is in a three-column layout on large screens and close to full-width otherwise.” Practicality-wise, this can be a sane solution.

You might find that some automated responsive image solutions, which have no way of knowing your layout, make a guess — something like sizes="(max-width: 1000px) 100vw, 1000px". This is just saying, “Hey we don’t really know much about this layout, but we’re gonna take a stab and say, worst case, the image is full-width, and let’s hope it never renders larger than 1000px”.

Abstracting sizes

I’m sure you can imagine how easy it is to not only get sizes wrong, but also have it become wrong over time as layouts change on your site. It may be smart for you to abstract it using a templating language or content filter so that you can change the value across all of your images more easily.

I’m essentially talking about setting a sizes value in a variable once, and using that variable in a bunch of different <img> elements across your site. Native HTML doesn’t offer that, but any back end language does; for instance, PHP constants, Rails config variables, the React context API used for a global state variable, or variables within a templating language like Liquid can all be used to abstract sizes.

<?php
  // Somewhere global
  $my_sizes = "";
?>

<img
  srcset=""
  src=""
  alt=""
  sizes="<?php echo $my_sizes; ?>"
/>

“Browser’s choice”

Now that we have a sizes attribute in place, the browser knows what size (or close to it) the image will render at and can work its magic. That is, it can do some math that factors in the pixel density of the screen, and the size that the image will render at, then pick the most appropriately-sized image.

The math is fairly straightforward at first. Say you’re about to show an image that is 40vw wide on a viewport that is 1200px wide, on a 2x pixel-density screen. The perfect image would be 960 pixels wide, so the browser is going to look for the closest thing it’s got. The browser will always calculate a target size that it would prefer based on the viewport and pixel-density situations, and what it knows from sizes, and compare that target to what it’s got to pick from in srcset. How browsers do the picking, though, can get a little weird.

A browser might factor more things into this equation if it chooses to. For example, it could consider the user’s current network speeds, or whether or not the user has flipped on some sort of “data saver” preference. I’m not sure if any browsers actually do this sort of thing, but they are free to if they wish as that’s how the spec was written. What some browsers sometimes choose to do is pull from cache. If the math shows they should be using a 300px image, but they already have a 600px in local cache, they will just use that. Smart. Room for this sort of thing is a strength of the srcset/sizes syntax. It’s also why you always use different sizes of the same image, within srcset: you’ve got no way to know which image is going to be selected. It’s the browser’s choice.

This is weird. Doesn’t the browser already know this stuff?

You might be thinking, “Uhm why do I have to tell the browser how big the image will render, doesn’t it know that?” Well, it does, but only after it’s downloaded your HTML and CSS and laid everything out. The sizes attribute is about speed. It gives the browser enough information to make a smart choice as soon as it sees your <img>.

<img
  data-sizes="auto"
  data-srcset="
    responsive-image1.jpg 300w,
    responsive-image2.jpg 600w,
    responsive-image3.jpg 900w"
  class="lazyload" 
/>

Now you might be thinking, “But what about lazy-loaded images?” (as in, by the time a lazy-loaded image is requested, layout’s already been done and the browser already knows the image’s render size). Well, good thinking! Alexander Farkas’ lazysizes library writes out sizes attributes automatically on lazyload, and there’s an ongoing discussion about how to do auto-sizes for lazy-loaded images, natively.

sizes can be bigger than the viewport

Quick note on sizes. Say you have an effect on your site so that an image “zooms in” when it’s clicked. Maybe it expands to fill the whole viewport, or maybe it zooms even more, so that you can see more detail. In the past, we might have had to swap out the src on click in order to switch to a higher-res version. But now, assuming a higher-res source is already in the srcset, you can just change the sizes attribute to something huge, like 200vw or 300vw, and the browser should download the super-high-res source automatically for you. Here’s an article by Scott Jehl on this technique.

↩️ Back to top


Using <picture>

Hopefully, we’ve beaten it into the ground that <img srcset="" sizes="" alt=""> is for serving differently-sized versions of the same image. The <picture> syntax can do that too, but the difference here is that the browser must respect the rules that you set. That’s useful when you want to change more than just the resolution of the loaded image to fit the user’s situation. This intentional changing of the image is usually called “art direction.”

Art Direction

<picture>
  <source 
    srcset="baby-zoomed-out.jpg"
    media="(min-width: 1000px)"
  />
  <source 
    srcset="baby.jpg"
    media="(min-width: 600px)"
  />
  <img 
    src="baby-zoomed-in.jpg" 
    alt="Baby Sleeping"
  />
</picture>

This code block is an example of what it might look like to have three stages of an “art directed” image.

  • On large screens, show a zoomed-out photo.
  • On medium screens, show that same photo, zoomed in a bit.
  • On small screens, zoom in even more.

The browser must respect our media queries and will swap images at our exact breakpoints. That way, we can be absolutely sure that nobody on a small screen will see a tiny, zoomed-out image, which might not have the same impact as one of the zoomed-in versions.

Here’s a demo, written in Pug to abstract out some of the repetitive nature of <picture>.

Art direction can do a lot more than just cropping

Although cropping and zooming like this is the most common form of art direction by far, you can do a lot more with it. For instance, you can:

Sky’s the limit, really.

Combining source and srcset

Because <source> also uses the srcset syntax, they can be combined. This means that you can still reap the performance benefits of srcset even while swapping out visually-different images with <source>. It gets pretty verbose though!

<picture>
  <source 
    srcset="
      baby-zoomed-out-2x.jpg 2x,
      baby-zoomed-out.jpg
    "
    media="(min-width: 1000px)"
  />
  <source 
    srcset="
      baby-2x.jpg 2x,
      baby.jpg
    "
    media="(min-width: 600px)"
  />
  <img 
    srcset="
      baby-zoomed-out-2x.jpg 2x
    "
    src="baby-zoomed-out.jpg"
    alt="Baby Sleeping"
  />
</picture>

The more variations you create and the more resized versions you create per variation, the more verbose this code has to get.

Fallbacks for modern image formats

The <picture> element is uniquely suited to being able to handle “fallbacks.” That is, images in cutting-edge formats that not all browsers might be able to handle, with alternative formats for browsers that can’t load the preferred, fancy one. For example, let’s say you want to use an image in the WebP format. It’s a pretty great image format, often being the most performant choice, and it’s supported everywhere that the <picture> element is, except Safari. You can handle that situation yourself, like:

<picture>
  <source srcset="party.webp">
  <img src="party.jpg" alt="A huge party with cakes.">
</picture>

This succeeds in serving a WebP image to browsers that support it, and falls back to a JPEG image, which is definitely supported by all browsers.

Here’s an example of a photograph (of me) at the exact same size where the WebP version is about 10% (!!!) of the size of the JPEG.

How do you create a WebP image? Well, it’s more of a pain in the butt than you’d like it to be, that’s for sure. There are online converters, command line tools, and some modern design software, like Sketch, helps you export in that format. My preference is to use an image hosting CDN service that automatically sends images in the perfect format for the requesting browser, which makes all this unnecessary (because you can just use img/srcset).

WebP isn’t the only player like this. Safari doesn’t support WebP, but does support a format called JPG 2000 which has some advantages over JPEG. Internet Explorer 11 happens to support an image format called JPEG-XR which has different advantages. So to hit all three, that could look like:

<picture>
  <source srcset="/images/cereal-box.webp" type="image/webp" />
  <source srcset="/images/cereal-box.jp2" type="image/jp2" />
  <img src="/images/cereal-box.jxr" type="image/vnd.ms-photo" />
</picture>

This syntax (borrowed form a blog post by Josh Comeau) supports all three of the “next-gen” image formats in one go. IE 11 doesn’t support the <picture> syntax, but it doesn’t matter because it will get the <img> fallback which is in the JPEG-XR format it understands.

Estelle Weyl also covered this idea in a 2016 blog post on image optimization.

↩️ Back to top


Where do you get the differently-sized images?

You can make them yourself. Heck, even the free Preview app on my Mac can resize an image and “Save As.”

The Mac Preview app resizing an image, which is something that literally any image editing application (including Photoshop, Affinity Designer, Acorn, etc.) can also do. Plus, they often help by exporting the variations all at once.

But that’s work. It’s more likely that the creation of variations of these images is automated somehow (see the section below) or you use a service that allows you to create variations just by manipulating the URL to the image. That’s a super common feature of any image hosting/image CDN service. To name a few:

Not only do these services offer on-the-fly image resizing, they also often offer additional stuff, like cropping, filtering, adding text, and all kinds of useful features, not to mention serving assets efficiently from a CDN and automatically in next-gen formats. That makes them a really strong choice for just about any website, I’d say.

Here’s Glen Maddern in a really great screencast talking about how useful Image CDNs can be:

Design software is becoming more aware that we often need multiple copies of images. For example, the exporting interface from Figma is pretty nice, where any given selection can be exported. It allows multiple exports at once (in different sizes and formats) and remembers what you did the last time you exported.

Exporting in Figma

Automated responsive images

The syntax of responsive images is complex to the point that doing it by hand is often out of the question. I’d highly recommend automating and abstracting as much of this away as possible. Fortunately, a lot of tooling that helps you build websites knows this and includes some sort of support for it. I think that’s great because that’s what software should be doing for us, particularly when it is something that is entirely programmatic and can be done better by code than by humans. Here are some examples…

  • Cloudinary has this responsive breakpoints tool including an API for generating the perfect breakpoints.
  • WordPress generates multiple versions of images and outputs in the responsive images syntax by default.
  • Gatsby has a grab-bag of plugins for transforming and implementing images on your site. You ultimately implement them with gatsby-image, which is a whole fancy thing for implementing responsive images and other image loading optimizations. Speaking of React, it has component abstractions like “An Almost Ideal React Image Component” that also does cool stuff.
  • Nicolas Hoizey’s Images Responsiver Node module (and it’s Eleventy plugin) makes a ton of smart markup choices for you, and pairs nicely with a CDN that can handle the on-the-fly resizing bits.
  • These are just a few examples! Literally anything you can do to make this process easier or automatic is worth doing.
Here’s me inspecting an image in a WordPress blog post and seeing a beefy srcset with a healthy amount of pre-generated size options and a sizes attribute tailored to this theme.
A landing page for gatsby-image explaining all of the additional image loading stuff it can do.

I’m sure there are many more CMSs and other software products that help automate away the complexities of creating the responsive images syntax. While I love that all this syntax exists, I find it all entirely too cumbersome to author by hand. Still, I think it’s worth knowing all this syntax so that we can build our own abstractions, or check in on the abstractions we’re using to make sure they are doing things correctly.

  • The object-fit property in CSS controls how an image will behave in its own box. For example, an image will normally “squish” if you change the dimensions to something different than its natural aspect ratio, but object-fit can be used to crop it or contain it instead.
  • The object-position property in CSS allows you to nudge an image around within its box.

What about responsive images in CSS with background images?

We’ve covered exactly this before. The trick is to use @media queries to change the background-image source. For example:

.img {
  background-image: url(small.jpg);
}
@media 
  (min-width: 468px),
  (-webkit-min-device-pixel-ratio: 2), 
  (min-resolution: 192dpi) {
  .img {
    background-image: url(large.jpg);
  }
}

With this CSS syntax, depending on the browser conditions, the browser will only download one of the two images, which achieves the same performance goal that the responsive images syntax in HTML does. If it helps, think of the above as the CSS equivalent of the <picture> syntax: the browser must follow your rules and display what matches.

If you’re looking to let the browser choose the best option, like srcset/sizes, but in CSS, the solution is ultimately going to be the image-set() function. There’s two problems with image-set(), today, though:

  • Support for it isn’t there yet. Safari’s implementation leads the pack, but image-set() has been prefixed in Chrome for eight years, and it’s not there at all in Firefox.
  • Even the spec itself seems behind the times. For example, it only supports x descriptors (no w, yet).

Best to just use media queries for now.

Do you need to polyfill?

I’m pretty meh on pollyfilling any of this right this moment. There is a great polyfill though, called Picturefill, which will buy you full IE 9-11 support if you need that. Remember, though, that none of this stuff breaks to the point of not displaying any image at all in non-supporting browsers, assuming you have an <img src="" alt=""> in there somewhere. If you make the (fairly safe) assumption that IE 11 is running on a low-pixel-density desktop display, you can make your image sources reflect that by default and build out from there.

Other important image considerations

  • Optimizing quality: The point of responsive images is loading the smallest, most impactful resource that you can. You can’t achieve that without effectively compressing your image. You’re aiming for a “sweet spot” for every image, between looking good and being light. I like to let image hosting services solve this problem for me, but Etsy has a really great writeup of what they’ve been able to accomplish with infrastructure that they built themselves.
  • Serving from CDNs: Speaking of image hosting services, speed comes in many forms. Fast servers that are geographically close to the user are an important speed factor as well.
  • Caching: What’s better than loading less data over the network? Loading no data at all! That’s what HTTP caching is for. Using the Cache-Control header, you can tell the browser to hang on to images so that if the same image is needed again, the browser doesn’t have to go over the network to get it, which is a massive performance boost for repeat viewings.
  • Lazy loading: This is another way to avoid loading images entirely. Lazy loading means waiting to download an image until it is in or near the viewport. So, for example, an image way far down the page won’t load if the user never scrolls there.

Other good resources

(That I haven’t linked up in the post already!)

Browser Support

This is for srcset/sizes, but it’s the same for <picture>.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
3838No16TP

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12212312217.4