The srcset attribute of HTML images: A detailed guide
Explore how the srcset attribute really works under the hood, with its two descriptors — density (x) and width (w).
As a frontend developer, when you present images to your users in the form of <img> elements, you need to be wary of the multitude of differences in the capabilities of those users' devices.
Some users might have large screens with high resolutions, some might have retina screens (with even higher resolutions packed in less physical space), some might have high-latency networks, some might have a 2x zoom enabled, and so on and so forth.
Now although, it is technically possible to deliver one and only one image to every user (with any kind of device) — and a thing that has been practiced for quite long in HTML — there are obvious disadvantages to it.
Certainly, as just stated, users' devices aren't all equal. Some devices are more capable than others. Serving a high-res image on a low-res device won't be a good decision, would it? Conversely, serving a low-res image on a high-res device won't be good either, right?
Serving images based on respecting the end user's device's capabilities and current configuration is both good in terms of wisely consuming the end user's resources and also in terms of delivering the best experience to those with the best devices. This practice of delivering the most appropriate images based on a device's capabilities and configuration is commonly referred to as using responsive images — a core tenet of responsive web design.
The srcset attribute of <img> is one way of achieving this.
What is srcset?
First, let's recall what the src attribute of <img> does. The src attribute simply defines the source of the underlying image, i.e. a URL pointing to an image file (or maybe even containing the data in the URL itself, in the case of data: URLs).
Reading the name srcset, there seems to be some parallel between src and srcset. Turns out, there actually is! The srcset attribute of <img> is used to define a set of different sources to choose from (in contrast to just one source).
But why care about multiple image sources? Here's the thing...
srcset allows the browser to choose the most appropriate image for the <img> element from the given set of sources. In other words, it allows the browser to responsively serve an image to the user, selecting the best candidate as per his device's capabilities and current configuration. This is practically only possible if there are multiple choices to choose from.
What goes inside srcset?
For src, recall that we take the source URL of the image, put it inside the src attribute, and we're done. srcset, however, works differently, as you can probably guess.
In srcset, when defining a given source (because there are usually multiple of them), we begin with the source URL and then include a descriptor for providing some information regarding the source.
The HTML standard defines two descriptors: x and w.
You shall get to know of both of these descriptors and the precise intuition behind them as this article progresses. For now, let's focus on the syntax of srcset:
sourceURL1 descriptor1, sourceURL2 descriptor2, ...Each source is comprised of a source URL and a descriptor. Multiple sources are separated from one another using a comma (,). It's possible to omit the descriptor but only once; in that case, it defaults to the value 1x (more on what this means in the next section).
srcset does NOT matter. This is because the browser considers all the given sources in order to determine the winner.Nice terminology used by the HTML standard
The HTML standard uses pretty nice terminology for srcset worth discussing. It says that srcset's value is comprised of one or more image candidate strings, separated by commas (,).
So in the syntax specification above
sourceURL1 descriptor1is an image candidate string. And so is
sourceURL2 descriptor2An image candidate string basically represents an image candidate from which the browser will eventually choose one winner, to be rendered by the underlying <img> element.
Important terminology
Let's go through some diverse concepts quickly before getting deeper into the intuition underpinning the x and w descriptors.
Device pixels vs. CSS pixels
There's a fine line between what is actually a pixel from the perspective of the underlying device vs. what we call a pixel in CSS. They are referred to as device pixels and CSS pixels, respectively.
So what exactly are these?
Device pixels are the actual, physical pixels behind the screen of a device. The number of these physical pixels can easily be inspected by referring to the device's screen resolution.
CSS pixels are an abstraction made by browsers for us to think of pixels in the same way across all devices. They're basically units of measurement — a CSS pixel takes, more or less, the same physical space on every device. It's what represents the standard px unit in CSS.
In the following discussion, whenever I use the term "px" (that is, the px unit), it'll be referring to CSS pixels. For example, 300px means 300 CSS pixels. For device pixels, I'll just use the term "device pixels".
Device pixel ratio and pixel density
Since a CSS pixel is a unit of measurement, it doesn't have to consume exactly one device pixel; it can well consume two device pixels, or maybe three, or even two and a half, and so on.
Device pixel ratio (DPR) refers to the number of device pixels that fit in one CSS pixel on a given device.
For instance, let's say the screen resolution of a smartphone is 720 x 1080. Here, 720 represents the device's width in terms of its (actual) device pixels while 1080 represents its height in device pixels. But the screen width reported in the browser on this device, in terms of CSS pixels, is 360 (width) x 540 (height).
In other words, each CSS pixel actually takes up two device pixels on the screen (i.e. 360 CSS pixels span 720 device pixels). Thus, we say that the DPR in this case is 2.
Related to the idea of DPR is that of pixel density which traditionally refers to the number of pixels packed per inch on the screen. The main difference between pixel density and DPR is that pixel density is not usually expressed as a ratio but rather as a value with a unit; the unit is typically PPI (pixels per inch).
The x descriptor
So now that you know what is meant by a device pixel, CSS pixel, DPR, and pixel density, it's time to commence the discussion on descriptors, starting with x.
The x descriptor, also known as the density descriptor, specifies the desired DPR at which the corresponding image source should be used. Its format is <num>x (here, <num> is the DPR).
For instance, you might want a 300 x 300 image to be displayed on a normal density screen but a 600 x 600 image on a high density screen with a DPR of 2. This is because in the latter case, more pixels could be fit into a small area and so you'll definitely want to render a high-res image.
In this case, you'll specify two sources in srcset:
- the 300 x 300 source with the density descriptor
1x, and - the 600 x 600 source with the descriptor
2x.
2x literally means that the corresponding image is "2 times" the size of the base image (which is 300 x 300).Once the browser chooses an image source from srcset based on the device's DPR, it pulls the image and then reads its dimensions. It renders the image at such dimensions (in units of px) that the DPR value is respected. This is a straightforward idea.
For example, if the chosen source's descriptor reads 2x and the underlying image's intrinsic (original) width is 300 pixels — that is, it can consume a total of 300 device pixels — the browser will display the image at a width of 150px (150 CSS pixels) by default. In this way, 300 device pixels get fit into 150px, thereby respecting the given DPR of 2.
If this is confusing you, don't worry. Continue reading on, for things will slowly start to come together as you go through the examples presented below.
Example of the x descriptor
In this section, we'll be embedding an <img> element with a srcset attribute, where each image source has a different density descriptor. The idea is to provide the browser with multiple image sources to choose from — the choice to be based upon the device's DPR.
Here's the base image I'm using, namely shapes-300.png: (I've deliberately added 300 to the name to indicate that the intrinsic width of the image is 300 pixels, which can also be seen in the image itself.)

srcset.In addition to this, I'm using two scaled-up versions of this image: shapes-600.png (600 pixels wide) and shapes-900.png (900 pixels wide).
With this setup, let's get to the HTML code:
<img srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x">There are three different image sources catering to different DPRs via the density descriptors 1x, 2x, and 3x. shapes-300.png will get selected probably when the DPR is 1, shapes-600.png will get selected when it is 2, and shapes-900.png will get selected when it is 3.
Given that my laptop has a DPR of 1 and my current zoom level is also 1 (more on zoom levels later on), here's the output I get when I open up the webpage linked above:

It's clear by the result that the chosen image source is shapes-300.png (this is evident by the '300' at the top-left corner in the image).
Image caching messes with srcset experimentation!
Browsers have a tendency to cache images, when possible, in order to conserve computational resources. Unfortunately, this might skew the results of experimenting with srcset.
To avoid this issue, make sure that caching is disabled for the time being while you experiment around with srcset.
In order to do so, head over to "Developer Tools" and navigate to the "Network" tab. From here, check the option that reads "Disable cache", as shown below:

This instructs the browser to not cache any resource, including images.
Now, the next time you load any of the live example links here, remember to open up Developer Tools and ensure that caching is disabled. For the safe side, reload the webpage once more (after launching Developer Tools) so that you're certain that fresh choices are made.
Now, if I zoom into the page (using Ctrl + +) so that the zoom factor increases from 1 to 1.5 and then reload the webpage, the rendered image changes to shapes-600.png (evident by the "600" label in the image):

But why did the image change by virtue of zooming in?
Well, that's because when we zoom in, the DPR effectively changes. In the case above — zooming by a factor of 1.5 — it changed to 1.5. Now, with this new DPR, when you reload the webpage, the browser realizes that the best choice is the image corresponding to the density descriptor of 2x, i.e. shapes-600.png.
But why is that so?
you say. Why is the
Well, it's simple.2x source chosen here instead of the 1x one?
If the 1x image is chosen, it'll be low-res for the current device configuration. A 1x image is meant for a device configuration with a DPR of 1 (i.e. 1 CSS pixel spans 1 device pixel).
However, currently, with the zoom applied, the browser can render more device pixels in a given area — 1.5 device pixels in 1 CSS pixel, so to speak. So it chooses a high-res image — shapes-600.png in this case — simply to use the current configuration to its full potential.
2x so it's ideally meant for a DPR-2 setting, not a DPR-1.5 setting. However, the browser chooses it regardless because it still allows the current 1.5 DPR to be utilized fully.If I increase the zoom to a factor of 2, the result will be the same as when I had a zoom factor of 1.5. That is, the rendered source will be shapes-600.png. The image's density descriptor (i.e. 2x) matches perfectly with the current DPR (which is also 2).
Revving up the zoom further would again change the result. Here's the output when the zoom factor is 2.5 (i.e. the DPR is now effectively 2.5):

Again, the browser realizes that since the DPR is 2.5, the most appropriate image is the one tagged with the descriptor 3x, i.e. shapes-900.png. And so it renders it in order to use its rendering capabilities to the full extent.
As this example demonstrates, by zooming into or out of a webpage, we are able to change the DPR and, consequently, influence the choice of the image made by the browser for the underlying <img> element.
So this wraps it up for the x descriptor. (Finally!)
The w descriptor
The x descriptor is sufficient for most use cases. It's a simple way of providing the browser with a multitude of images with different resolutions to choose from. However, sometimes we need more control.
Imagine you have a webpage showing a large banner image as demonstrated below, spanning the entire width available to it (which is why it's called a "banner"):

The <img> element for this banner is as follows:
<img srcset="banner-1500.png 1x, banner-3000.png 2x" style="width: 100%">They're are two sources to choose from: banner-1500.png (1500 pixels wide) for a DPR-1 setting and banner-3000.png (3000 pixels wide) for a DPR-2 setting. The width: 100% style gets the image to span all its available width.
Let's see the problem with this setup.
Suppose your webpage's setting is such that the screen is 683 CSS pixels wide, while the actual width is 1366 device pixels — giving a DPR of 2.
Which image do you think will the browser choose in this case? Well, you can expect the second image, banner-3000.png, to be loaded because the current DPR is 2 and banner-3000.png has the 2x descriptor.
Let's confirm this. When I open up the linked example above on the stated setting (where the webpage is 683px wide and the screen is 1366 device pixels wide), I get the following output:

As you can see, the chosen image is banner-3000.png (this is evident by the label "3000" that I added to the image).
And that's the problem. Let's understand it in detail...
Recall that the width of the webpage is 683px. Likewise, even if we render an image that is intrinsically 1366 pixels wide on it, we'd be respecting the current DPR of 2 (1366 pixels packed up in 683 CSS pixels). So, certainly a 1500 pixels wide image would already be more than just ideal for the current setting.
In other words, even if the browser renders the 1500 pixels image, banner-1500.png, in the 683px width, it'd be maxing out the available pixel density on the current setting.
But what it chooses instead: banner-3000.png, that's 3000 pixels wide! That's an absolute overkill!
The reason why the browser chooses banner-3000.png is simply because we instructed it to do so by virtue of the 2x descriptor. The browser doesn't know anything about the image except for that it should be chosen if the DPR is 2.
This problem is exactly why the w descriptor exists.
The w descriptor, also known as the width descriptor, specifies the intrinsic width of the corresponding image source. Its format is <width>w (here, <width> is an integer representing the intrinsic width).
For instance, a value of 300w means that the corresponding image is originally 300 pixels wide (that is, it contains information for a total of 300 pixels in its width). Similarly, for an image that is originally 2400 pixels wide, we'll use the value 2400w.
The width descriptor builds upon the same ideas of DPR that you saw above for the density descriptor (x), albeit this time the browser computes the desired DPR for the image source itself instead of you explicitly writing it out like 1x, 2x, or 3x, and so on.
w descriptors down to x descriptors behind the scenes.However, there's something missing here...
The browser can NOT compute the DPR that each image caters to solely based on the given w value; it needs to know the width of the slot, in CSS pixels, as well where the image will be displayed.
For this, we have the sizes attribute of the <img> element.
The sizes attribute
Where the width descriptor helps us specify the actual width of an image, sizes helps us specify the width of the slot where the image is to be rendered. It basically completes the equation when it's paired with the w descriptor.
Here's the syntax of sizes:
(mediaQuery1) slotWidth2, (mediaQuery2) slotWidth2, ..., defaultSlotWidthWe have a sequence of source sizes, separated using commas (,). So, (mediaQuery1) slotWidth2 is a source size, and so is defaultSlotWidth.
Each source size, except for the last one, is comprised of a media query condition followed by the width of the image's slot, in CSS pixels. If the media query is met, the corresponding slot width is put into action.
px, like for e.g. em, the browser resolves it down to px (just like is the norm in CSS).The last source size is special. It gives the default slot width. As you can see, it doesn't have any media query in it (since it doesn't need one; it's the default, remember?). Most importantly, sizes must end with a default slot width, that is, a source size without a media query.
Let's consider an example to help understand what's going on. Consider the following value of sizes:
(max-width: 600px) 500px, 980pxThis means that when the viewport's width is less than or equal to 600px (denoted by max-width in the media query), the image slot's width is 500px. Otherwise, it's 980px. Simple.
The browser uses this slot width to then compute the DPRs at which the corresponding image sources in srcset must be rendered. Following is an example.
Suppose you have two sources in srcset, labeled 600w and 1500w. The viewport width is 600px. This matches the query (max-width: 600px) 500px, and so the slot width in this case is 500px. The corresponds to a DPR of 1.2 for the 600w source (600 / 500) and a DPR of 3 for the 1500w source (1500 / 500).
However, if the viewport's width is 700px, then the (max-width: 600px) media query fails and, likewise, the slot width defaults to 980px. Thereafter, the 600w source corresponds to a DPR of 0.61 (600 / 980) while the 1500w source corresponds to that of 1.53 (1500 / 980).
If you're having a hard time understanding all of this, no need to panic. Shortly below, I'll be presenting some examples which will help you understand the width descriptor, sizes, and the idea of slots. For now, there are two important points worth clarifying with respect to sizes.
The order of source sizes matters
Keep in mind that the order of the source sizes in sizes matters.
This is because the browser evaluates them from start to end and as soon as it finds a matching media query, it stops right there and takes the corresponding slot width, or else proceeds with the default slot width (i.e. the last size value).
So, for instance, the following sizes value:
(max-width: 300px) 250px, (max-width: 600px) 500px, 980pxis different from the one below:
(max-width: 600px) 500px, (max-width: 300px) 250px, 980pxIn fact, in this value, the (max-width: 300px) rule will never apply because in case its condition does match, it will get ignored in light of the preceding (max-width: 600px) rule.
Said another way, if the media query (max-width: 300px) yields a match for the webpage's current settings, then (max-width: 600px) would too. And because the order of values in sizes matters, (max-width: 600px) would always win.
sizes, making sure that no source size gets shadowed by any preceding values!Slot widths can't use the % unit
As you may know, the % (percent) unit is really popular in CSS. It's an essential part of responsive web design. Everyone wants to use % units almost everywhere.
However, when it comes to specifying slot widths in the sizes attribute, % is NOT allowed. So, for instance, you can't do the following:
(max-width: 300px) 100%, (max-width: 600px) 80%, 50%The reason for this decision is because when the browser is resolving <img>s (deciding which source to request and render), it hasn't yet begun the document's layout routine. It needs to have layout information with it before a % width can be resolved. And this is exactly why % is disallowed in sizes — because the layout is still pending.
However, with units such as px, em, vw, the browser can immediately resolve the given lengths without having to go through layout.
For instance, in resolving 80vw, the browser only needs to multiply the value 0.8 (80 / 100) with the viewport's width, which it already knows (even before the underlying HTML page is requested itself, let alone the layout).
Alright, now let's turn to some examples of the w descriptor.
Example of the w descriptor and sizes
In the discussion above, you saw an example of a banner image where the browser was rendering a very high-res variant, banner-3000.png, due to the 2x descriptor even though there was absolutely no need to.
Let's review the <img> element for it:
<img srcset="banner-1500.png 1x, banner-3000.png 2x" style="width: 100%">Using what you just learned regarding the w descriptor, let's fix the issue here.
First things first, we need to determine a good enough slot width for the banner image — that is, what would be a good measure for the width of the rendered banner image in terms of CSS pixels. This will help define sizes.
Fortunately, this is easy in this case. Since the banner must span almost the entire viewport width (excluding the default margins around <body>), a good candidate for the slot width is 100vw. And because the slot width would still remain 100vw as the screen size changes, there is no need for multiple source sizes (with media queries).
So this defines sizes. Now, let's get to srcset.
Obviously, there are only two images to tackle, banner-1500.png and banner-3000.png. The former is originally 1500 pixels wide whereas the latter is 3000 pixels wide. Likewise, the width descriptors will become 1500w and 3000w, respectively.
w descriptor is a no-brainer — just look up the original width of the image and you're done.All in all, this gives us the following <img> element: (I've formatted it for the sake of readability and boldened the important parts.)
<img
sizes="100vw"
srcset="banner-1500.png 1500w, banner-3000.png 3000w"
style="width: 100%">Let's see what the browser has to render for this <img>, going back to the same setting that we used before — 683px (CSS pixels) wide webpage on a 1366 pixels (device pixels) wide screen:

w descriptorVoila! The browser renders the 1500 pixels variant, banner-1500.png. But let's dig deeper and find out why...
Recall that at the current setting, the DPR is 2. The source banner-1500.png reads 1500w and the slot width is 100vw (due to sizes="100vw"). Since the viewport's width is 683px, 100vw becomes 683px. Dividing 1500 (from 1500w) by 683 (from 100vw) gives 2.2.
This essentially means that if the device's DPR is 2.2, banner-1500.png should be used. And because the DPR is actually 2, banner-1500.png is already sufficient enough to be rendered.
Zooming in doesn't help here
It's worth noting that at the given setting, even if we zoom into the webpage, the image rendered will always be banner-1500.png. This makes perfect sense because regardless of the zoom factor, the screen of the device is still the same — it can only represent 1366 pixels max.
By rendering the 1500 pixels source, banner-1500.png, the browser is already maxing out the resolution of the screen; and there's absolutely no reason to go beyond that and bring in the 3000 pixels source.
An important point to note in the example above is that sizes="100vw" does NOT specify the rendered width of the image; it only specifies the width of the slot of the image. This is a paramount distinction to make.
The slot width is only used by the browser to compute a DPR value corresponding to the given image source; it's not used in the actual rendered width of the image.
It's easy to confirm this in the example above. The sizes="100vw" resolves to a width of 683px, right? However, the rendered width of the image is 667px. This discrepancy is a courtesy of the default margins applied to the <body> element and the width: 100% inline style of the <img>.
If you remove the width: 100% inline style (which forces the image's rendered width to be precisely as large as its parent's width), the image will render at the normal width of 683px, overflowing out of the browser window.
Should you use x or w in srcset?
Now this is a very good question. After all, it can get confusing whether you should opt for the simpler x descriptor in srcset or the more powerful w descriptor.
Well, there isn't a right or wrong choice here. It depends on the level of control you desire and the level of simplicity you want in laying out an <img>.
The table below summarizes when to use what:
Density descriptor (x) | Width descriptor (w) |
|---|---|
|
|
Including the src attribute
When you set the srcset attribute on an <img> element, it's still desirable to set src. Why?
you ask. Mainly to cater to old, legacy browsers that don't support srcset (honestly, very few of them are remaining nowadays but still).
But there's a bonus point to src when used in tandem with srcset. That is, the browser assumes the source provided in src to have a 1x descriptor.
So, for example, if you have the following <img> element, with both a srcset and src (for compatibility with browsers unable to recognize srcset):
<img
srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x"
src="shapes-300.png">you can remove the shapes-300.png 1x part from srcset because src already means the same thing.
<img
srcset="shapes-600.png 2x, shapes-900.png 3x"
src="shapes-300.png">In this example, the <img> element is actually presenting three sources to the browser: shapes-300.png with the density descriptor 1x (via the src attribute), shapes-600.png with 2x, and shapes-900.png with 3x.