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
srcset
Using 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.
<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.
srcset
/ w
+ sizes
Using 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.
sizes
Creating accurate 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:
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):
- At the largest size: there is 9rem of explicit spacing, so the image is
calc(100vw - 9rem - 200px)
wide. If that column used afr
unit instead of200px
, 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.
sizes
Being more chill about 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”.
sizes
Abstracting 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
<picture>
Using 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:
- Dark-ify™ images for users in dark mode,
- avoid sending animated GIFs to users with a “prefers reduced motion” accessibility preference,
- re-arrange image content so that it all fits “above the fold” on short viewports,
- set a maximum resolution cap, to save users on 3×-plus devices a lot of bytes,
- send static, high-res, monochrome images to printers and e-ink devices.
Sky’s the limit, really.
source
and srcset
Combining 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.”
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:
- Cloudinary offers it
- Netlify offers it
- imgix offers it
- Image Optim offers it
- Filestack offers it
- Cloudflare offers it
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.
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.
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.
Related concepts
- 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, butobject-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 (now
, 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!)
- Eric Portis on the Cloudinary blog: Responsive images with ‘srcset’, ‘sizes’ and Cloudinary
- Eric Portis’ deep dive into Srcset and sizes
- Eric Portis on Smashing Magazine: Responsive Images Done Right: A Guide To
<picture>
Andsrcset
- MDN Guide: Responsive images
- Jason Grigsby’s big 10-part guide on the Cloudfour blog
- Scott Vandehey on the Cloudfour blog: Responsive Images the Simple Way
- The original W3C Community Group that fought for responsive images in browsers and got it done
- Pete LePage on the Google Developer Web Fundamentals Guide: Images
- Addy Osmani’s Essential Image Optimization eBook
- Elad Shechter’s Complete Guide to Responsive Images
- 📹 Mat Marquis’ conference talk: The Past, Present, and Future of Responsive Images
- Mat Marquis’ book Image Performance
- Jake Archibald’s The anatomy of responsive images
- 📹 Responsive Images, WordPress, and Cloudinary
- Andreas Bovens on Opera’s Developer Blog: Responsive Images: Use Cases and Documented Code Snippets to Get You Started
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
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
38 | 38 | No | 16 | TP |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
126 | 127 | 126 | 18.0 |
Is it just me or is this just too complicated for many images? Why not talk about how to keep things easy? Why not question whether so much markup and tooling is necessary? Must everyone of us now uses tooling for every tag? We don’t work for Google or Facebook. There are many many many of us who say no!
I like the guidelines from Jen Meiert for example. Then you can still use img. Of course! When it makes sense. Why are perspectives like this missing? I think this guide here is fine but it’s not complete if all it says is use more HTML.
I try to make the point many times in the article that automation is vitally important here. I would never advocate that you author this by hand every time. Personally, I hardly ever do that. I’m always looking for ways to abstract and automate it. Quote from the article:
But I also advocate for understanding how it works. This article is about understanding how it works and having a reference to check against when you need to check the syntax and understand what is happening.
Thank you for replying. When would you give your approval to use img and only img? Or do I misunderstand and that is still fine from your point of view?
What’s the holdup on the support for
image-set()
?! The@media
solution works (I’m using it on a high-volume site) but it’s clunky and verbose.We ended up with a Sass mixin for it. The basic idea is checking for
@support
forimage-set()
and falling back to media queries.Thanks for this, Chris! And because it’s not explicitly mentioned, am I right in saying:
We can combine the
srcset
andsizes
approach with the addition ofwidth
andheight
HTML attributes on ourimg
, so we get the added benefit of improving performance and preventing layout jank too?(The specific dimensions we provide in our
width
andheight
aren’t important as long as they represent the required aspect ratio, since all images in oursrcset
should share a common aspect ratio anyway.)My understanding is that this update is coming soon in Firefox and Chrome (if not already released).
AFAIK there’s no similar solution at present for the Art Direction use case.
So where do the “modern image formats (JPEG 2000, JPEG XR, and WebP)” fit into this fuster cluck. How much code must we write to insert a single image?This is getting ridiculous! I’d like to keep all my website on one server but that’s begining to sound impossible.
OK, talk to me like I’m an idiot. What’s the difference between
srcset
anddata-srcset
?