HTML Images - The srcset Attribute

Chapter 27 50 mins

Learning outcomes:

  1. What is the srcset attribute of <img>
  2. What are device pixels, CSS pixels, and device pixel ratio (DPR)
  3. Pixel density and the x descriptor (density descriptor)
  4. Example of the x descriptor
  5. Different widths and the w descriptor (width descriptor)
  6. The sizes attribute and its purpose
  7. Example of the w descriptor
  8. Should we use x or w in srcset
  9. Further reading resources

A need for responsive images

As web developers, when we present images to our users in the form of <img> elements, we 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 dealing with every single possible user with one, and only one, image is possible, and a thing that has been practiced for long in HTML, there are obvious disadvantages to it.

Certainly, as we just stated, every user's device isn't equal. Some devices are more capable than others while some are less. 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 current capabilities and configuration is commonly referred to as using responsive images.

In this chapter, we shall learn about the first installment of delivering images responsively — how to leverage the srcset attribute of the <img> element in deciding which of a bunch of images to present to the user depending on his/her device's current pixel density ratio.

In particular, we'll cover the following ideas throughout the rest of this chapter: what is srcset and why do we need it in addition to src; how srcset actually works; what are the x and w descriptors; and a whole bunch of other useful ideas.

So without spending any more of our time in anything else, let's begin the learning.

What is srcset?

In the very first chapter of this unit, HTML Images — Basics, we learnt about the src attribute of an <img> element. Recall what it does?

The src attribute simply defines the source of the underlying image, typically being a URL pointing to an image file.

Reading the name srcset, there seems to be some parallel between src and srcset. Turns out, there actually is:

The srcset attribute of an <img> element is used to define a set of different sources to choose from.

As the name can easily be dissected as 'src set', it's evident that srcset is meant to hold a bunch of different image sources in contrast to src which can only hold one.

So the first thing that we learn at this point is that srcset is meant to hold a set of multiple image sources.

But why even care about multiple image sources?

Why do we even need to use srcset in the first place?

Well, srcset allows a browser to choose the most appropriate image for an <img> element from the set of different images that we provide in it. In other words, it allows us to responsively serve images to our users, giving respect to their device's current environment.

Moving on, note that the individual image sources inside srcset are NOT mentioned in the same way as we do in src.

In src, we take the source URL, put it inside the src attribute, and we're done. In srcset, however, when defining a source, we need to begin with the source URL, as usual, but then after it have some descriptive information as well.

The HTML standard gives us two descriptors for providing this descriptive information:

  • x — specifies the pixel density under which the given image should be displayed.
  • w — specifies the intrinsic width of the image.

We'll cover both of these and the intuition behind them as this chapter progresses. For now, let's see the syntax of srcset's value.

Consider the following syntax specification of the value assigned to srcset:

sourceURL1 descriptor1, sourceURL2 descriptor2, ...

We essentially have a set of different sources separated by a comma (,). Each source is comprised of a source URL, followed by some whitespace, followed by the source's descriptor.

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).

Remember that the descriptor can be left out only once in srcset.

Nice terminology used by the HTML standard

The HTML standard uses quite nice of a terminology for srcset to aid us in discussing about it in an organized, formal way. It says that srcset's value is comprised of one or more image candidate strings, separated by commas (,).

Each image candidate string basically represents an image from which the browser will eventually choose a concrete image to display for the underlying <img> element.

Notice the term 'candidate' — it depicts the very fact that this string (a sequence of text) is merely a candidate for the ultimate winner of the underlying <img>'s slot.

It's not that we specify an image candidate string and it gets picked up out of nowhere; rather, it's a candidate which will be judged for qualification by the browser (after it makes sense of the environment where the image will be rendered).

The browser evaluates all the sources given in srcset, reading their descriptors, and picks the most appropriate one depending on a number of factors as listed below:

  • The pixel density (explained below) of the device's screen.
  • The zoom factor of the webpage. For e.g. a 2x zoom factor would double the device pixel ratio (explained below).
  • The condition of the user's network. For e.g. on a slow network, the browser might keep from requesting for a high-res image.

Note that the order of sources in srcset does NOT matter. This is because the browser goes over all the given sources in order to determine which one is the most appropriate.

Pixel density and the x descriptor

The first descriptor to explore is the x descriptor, which defines the pixel density at which the underlying image source should be used.

Now, before we can make sense of x, we should better spend a little bit of time understanding the difference between a device pixel and a CSS pixel and how that gives rise to the related idea of pixel density.

Let's go through these diverse concepts quickly before getting deeper into the intuition underpinning the x descriptor.

Device pixels vs. CSS pixels

There's a fine line between what we call a pixel in HTML/CSS vs. what is actually a pixel from the perspective of the underlying device.

In general discussion, two terms are commonly used to disambiguate between these differing concepts. We have device pixels and then we have CSS pixels.

So what exactly are these?

Device pixels are the actual physical pixels conceived behind the screen of a device.

The number of these physical, device 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.

A CSS pixel takes, more or less, the same physical space on every device. It's what represents the standard and extremely used px unit in webpages.

Device pixel ratio and pixel density

A CSS pixel doesn't have to consume exactly one device pixel; it can well consume two device pixels, or more than this, or even less.

This is an important characteristic of a device for a browser to know when rendering a webpage. It's commonly referred to as the device pixel ratio.

Device pixel ratio refers to the number of device pixels per every CSS pixel.

For instance, the screen resolution of the Redmi 9C smartphone is 720 x 1080. Here, 720 represents the device's width in terms of its actual (device) pixels while 1080 represents its height. Now, the screen width reported in the browser, in terms of CSS pixels, is 360 x 540.

In other words, each CSS pixel actually takes up two device pixels of the phone. Thus, we say that the device pixel ratio in this case is 2.

Device pixel ratio is sometimes also compactly referred to as pixel density, although pixel density is typically not expressed as a ratio but rather as a value with a unit.

So now that we know what are device pixels, CSS pixels, and device pixel ratios, it's time to get back to the x descriptor.

In very simple words:

The x descriptor, also known as a density descriptor, specifies the desired device pixel ratio (or pixel density) at which the corresponding image source should be used for the underlying <img> element.

Multiple image sources in srcset with density descriptors essentially enable us to specify a set of different images for the browser to choose from, serving the best image in terms of resolution for the device's screen.

For instance, we'll display a 300 x 300 image on a normal density screen but a 600 x 600 image on a high density screen (with a device pixel ratio of 2).

The syntax of the density descriptor is pretty straightforward: it must be a non-negative floating-point number followed by the letter x:

<num>x

This number, shown as <num> above, gives the desired device pixel ratio as discussed in the preceding definition.

As stated earlier in this chapter, when the descriptor part is omitted from a source in srcset, it's taken to be 1x.

Once the browser chooses an image source from a given srcset, each associated with a different density descriptor, it renders the image at a width (and height) that gives us the same pixel density as the one suggested.

For example, if an image source's density descriptor is 2x and the underlying image turns out to be actually 300 pixels wide, the browser will display the image at a width of 150 CSS pixels, should the image be chosen.

This choice is likely to be influenced by the true pixel density of the device, the current zoom factor (the greater the zoom, the greater the pixel density), and perhaps also by the network's speed (a slow network shouldn't be clogged up by requesting for a high-res image).

Alright, enough of theory, let's now get to consider some real examples of srcset and density descriptors in action.

Example of the x descriptor

In this section, we'll be creating a webpage with an <img> element that has srcset set on it, with each contained image source complemented with a different density descriptor.

The idea is to load and thereafter show the most appropriate image on the user's device based on its pixel density.

The image we'll use is the following image, namely shapes-300.png:

Image to demonstrate srcset
Image to demonstrate srcset

For the sake of reference, we've embedded the instrinsic width of the image at the top-right corner. For example, the image above is 300 pixels wide.

Being '300 pixels wide' means that this is the actual pixel information stored in the image. How many CSS pixels this image ultimately consumes on the browser is determined by the browser itself in addition to our CSS styles.

We have two other scaled-up versions of this same image: shaped-600.png, being 600 pixels wide, and shaped-900.png, being 900 pixels wide.

With this setup, let's get to our HTML code, focusing only on the <img> element:

<img srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x">

Here, we specify three different image sources catering to different device pixel densities by virtue of the density descriptors 1x, 2x, and 3x.

shapes-300.png will get selected (probably) when the device pixel ratio is 1, shapes-600.png will get selected when it is 2, and shapes-900.png will get selected when it is 3.

Seems quite simple, doesn't this?

Now before we explain anything, let's first see what the browser renders when we open up the HTML page:

Live Example

Image caching messes with srcset experimentation!

The browser has a tendency to cache images, when possible, in order to conserve computational resources. Unfortunately, this might skew the results of our experiments with srcset and different density descriptors.

To avoid this issue, we simply need to make sure that caching is disabled for the time being while we experiment around with srcset.

In order to do, head over to Developer Tools and navigate to the Network tab. From here, check the option that reads Disable cache, as shown below:

Disable cache option in Developer Tools
'Disable cache' option in Developer Tools

This instructs the browser to not cache any resource including images.

Now, the next time you open up any of the live example links in this chapter, make sure to open up the Developer Tools in another window to make sure that caching is disabled at all times. Also make sure to have the Developer Tools window open throughout the entire session while the live example window is open.

Given that our desktop device has a device pixel ratio of 1 and our current zoom level is also 1 (more on zoom levels later on), here's the output we get:

The output with a device pixel ratio of 1
The output with a device pixel ratio of 1

Now, if we zoom into the page so that the zoom factor is greater than 1 (1.5 in this case), here's the output we get:

The output with a device pixel ratio of 1.5
The output with a device pixel ratio of 1.5

As we zoom into the page, the device pixel ratio effectively changes — it become equal to 1.5 (with the browser showing a zoom level of 150%). At this point, the browser decides that the better image to choose is the one whose density descriptor is 2x, that is, shapes-600.png.

Moving forward, when we zoom further than this, taking it to a factor of 2.5, here's the output we get:

The output with a device pixel ratio of 2.5
The output with a device pixel ratio of 2.5

Akin to before, the browser here realizes that the most appropriate image is the one labeled with a density descriptor reading 3x, that is, shapes-900.png.

As this example demonstrates, by zooming into or out of a webpage, we are able to change the device pixel ratio and, consequently, influence the choice of the image made by the browser for the underlying <img>.

Another way to influence this decision-making is obviously to open up the example page linked above on different physical devices. Depending on the device's screen's specifications, different images get loaded.

Order of sources in srcset doesn't matter

Just to restate what we mentioned earlier in this chapter, the order of sources in srcset does NOT matter.

This means that the following HTML:

<img srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x">

is as good as this one, with the order of the sources modified:

<img srcset="shapes-600.png 2x, shapes-900.png 3x, shapes-300.png 1x">

The browser will process each of given sources to determine which one to pick; the order doesn't matter at all.

Density descriptors and the src attribute

When an <img> element has the srcset attribute set, containing sources complemented with density descriptors (x), it's still desirable to set src.

"Why?" you ask. Well, for two reasons:

  • To cater to old, legacy browsers that don't support srcset (honestly, very few of them are remaining nowadays).
  • To specify a 1x image source.

Notice the second point here: "To specify a 1x image source."

Hmm. What exactly does this mean?

Consider the following <img> element where we have srcset set in addition to src, with the value of srcset slightly modified:

<img srcset="shapes-600.png 2x, shapes-900.png 3x" src="shapes-300.png">

Most importantly, notice how we've omitted shapes-300.png 1x from srcset's value (unlike what we had before in the example from the previous section).

Let's review the code from the previous section, containing shapes-300.png in srcset:

<img srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x">

As highlighted here, we had shapes-300.png specified in srcset alongside the descriptor 1x (to use this image when the device pixel ratio is around 1).

But apparently, there is no src attribute in this example and so a browser not able to understand the srcset attribute will fail to load anything at all.

By specifying src, we gracefully address old browsers while delivering an optimum experience to the latest ones. In this case, setting src:

  • addresses old browsers that only understand src; and
  • delivers an optimum experience to latest ones, by taking part in the underlying image's source set.

When an <img> element has a srcset containing density descriptors, specifying src is equivalent to specifying a source with a density descriptor of 1x.

Likewise, the following code with the src attribute:

<img srcset="shapes-600.png 2x, shapes-900.png 3x" src="shapes-300.png">

is equivalent to the following:

<img srcset="shapes-300.png 1x, shapes-600.png 2x, shapes-900.png 3x">

Now the next time you see an <img> element in some HTML code, containing both srcset (with density-descriptors) and src, you'll know the reason behind this setup.

Different widths and the w descriptor

Usually, srcset with the x descriptor will serve most of our needs. It's a very simple approach to providing the browser with a multitude of images with different sizes (or resolutions) to choose from.

However, in some cases, we need more control. And for good.

Imagine we have a webpage showing a large banner image as demonstrated below:

Webpage with banner image
Webpage with banner image

The banner image spans the entire width available to it (which is essentially the reason we call it a 'banner' image).

The <img> element for this banner is as follows:

<img srcset="banner-1500.png 1x, banner-3000.png 2x" style="width: 100%">

We're using srcset to provide the browser with multiple images to choose from — banner-1500.png (1500px wide) for a 1x device pixel ratio and banner-3000.png (3000px wide) for a 2x device pixel ratio.

The width: 100% inline style serves to get the image to span all its available width. We already learnt about the width CSS property earlier in HTML Images — Basics: Responsive Images.

Let's see the problem with this setup.

Suppose we're on a 400px wide device in terms of CSS pixels, where the actual device's physical width, in device pixels, is 800px — giving us a device pixel ratio of 2.

Which image do you think will the browser most probably choose in this case?

Well, supposing that the network speed is fairly high, we can well expect the second image to be loaded, i.e. the banner-2x.png image because the device pixel ratio of the device is 2.

Let's even confirm this on our setup:

Live Example

In the page above, take the zoom up to a factor of 2 and then refresh the page to see which image is loaded. (Also make sure to have the Developer Tools window open, as explained above).

We get the 2x version loaded on our end despite the fact that our device's screen is only 1366 pixels wide and is, therefore, only capable enough of representing the 1500px image, banner-1500.png.

The output with a device pixel ratio of 2
The output with a device pixel ratio of 2

And there we have our problem.

Going back to the analogy of the 400 CSS pixels wide screen, even if we represent an 800px (device pixels) wide image on this device, we'll be getting that lovely, crisp display owing to the very fact that the pixel density of the screen (i.e. the device pixel ratio of 2) is being utilized to its full potential.

This means that we can comfortably deliver the 1500px wide image to this device and render it on the 400 CSS pixels on the screen, making sure that every device pixel counts.

However, recall what the browser shows to us — the 3000px wide image!

Why? Very simple: because we instruct it to load this image when the device pixel ratio is 2 via the value banner-3000.png 2x. And so the browser does load it.

This problem is exactly what the second possible descriptor in srcset, the w descriptor, was made for.

The w descriptor, also known as a width descriptor, specifies the original width of the corresponding image source which is ultimately used by the browser to compute the image's pixel density on the screen.

The width descriptor basically boils down to the same density descriptor that we saw above (we'll shortly see how), albeit this time the browser has more information to decide the best image to pick from the given set of sources.

The syntax of the w descriptor is also quite basic:

<int>w

We have a non-negative integer followed by the letter w.

We have an integer this time because image dimensions can only be in terms of pixels which are discrete entities.

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, if we have an image that is 2400 pixels wide, we'll use the value 2400w.

The width descriptor specifies the original width of the underlying image source but this is only part of the complete equation. The browser can NOT compute the pixel density that each image caters to solely based on its image. It needs to know the width of the slot where the image will be displayed as well.

For this, we have the sizes attribute of the <img> element.

The sizes attribute

Where width descriptors help us specify the actual widths of given images, sizes helps us specify the width of the frame where either of those images will be shown.

The sizes attribute completes the equation for the browser when an <img> element has a srcset containing width descriptors.

The sizes attribute is used to specify the width of the slot (or the frame) where the underlying image will eventually be displayed.

There is an exclusive syntax for the sizes attribute:

(mediaQuery1) slotWidth2, (mediaQuery2) slotWidth2, ..., defaultSlotWidth

We have a sequence of source size specifications, separated using commas (,). Each source size specification, except for the last one, is comprised of a media query followed by the width of the image's slot in that case.

Only if the media query condition gets fulfilled does its corresponding image slot size get put into action.

The last source size specification gives the default slot width. It doesn't have any media query. Most importantly, it's required. That is, sizes must end with a simple length value.

Not having the media query for the last source size makes absolute sense — it's the default source size specification and therefore doesn't need any conditions for it.

For example, the following value for sizes:

(max-width: 600px) 500px, 980px

means that when the viewport's width is less than or equal to 600px (CSS pixels), the image's slot's width is 500px.

The browser can then use this information to compute the effective pixel densities corresponding to the set of image sources provided in srcset.

For example, if we have an imaged labeled as 600w in srcset, and have a viewport width of 600px, this gives us a pixel density of 1.2x (600 / 500) for the image. Similarly, if we have an imaged labeled as 1500w on the same viewport width, it gives us a pixel density of 3x (1500 / 500).

However, if the viewport's width is 700px, the (max-width: 600px) media query fails and, likewise, the slot width becomes 980px. Thereafter, the 600w image will give us a pixel density of 0.61x (600 / 980) while the 1500w image will give that of 1.53x.

Keep in mind that the order of the individual values in sizes, i.e. source size specifications, matters.

So, the following sizes value:

(max-width: 300px) 250px, (max-width: 600px) 500px, 980px

is different from the one below:

(max-width: 600px) 500px, (max-width: 300px) 250px, 980px

In fact, in this value, the (max-width: 300px) rule will never apply; it case it matches, i.e. the viewport's width is less than or equal to 300px, it will get ignored in light of the preceding (max-width: 600px) rule.

Always make sure to craft the value of sizes carefully, making sure that no source size specification gets shadowed by any preceding specifications.

Following are a couple of points to note regarding the sizes attribute:

  • When srcset contains sources with width descriptors (w), it's a must to have sizes.
  • When srcset contains sources with density descriptors (x), if sizes is given, the browser ignores it.
  • The unit specifying the image slot's width in sizes can NOT be %.

The last point here is worth explaining, for many beginners might view this as counter-intuitive. You might ask: "Why can't we use % as the unit for giving the image slot's width?"

Why can't the sizes attribute of <img> have percent (%) units?

Percent (%) units are very common — or should we say, very very very common — in CSS and web designing. In part, they are the cornerstone of responsive designing on the web.

Everyone wants to use % units almost everywhere. However, when it comes to specifying the width of image slots in the sizes attribute of the <img> element, % is NOT allowed.

The reason for this decision is not really difficult to understand. It comes down to the way in which % units are resolved.

When the % unit is used on an element, in this case for specifying its width (had it been allowed in sizes), the length of the parent has to be known prior to being able to compute the element's width. Determining the width of the parent might also mean determining the width of its parent, which might depend on its parent, and so on.

All in all, we might have to render the entire document in order to see what an element's % width (also known as its relative width), resolve down to. And this is exactly why % is disallowed in sizes.

That is, resolving % in sizes would mean that the browser has to render the entire document before it can determine the corresponding width. However, if the browser goes on to do this, it would be deferring the loading of an image up until the point it has successfully rendered the entire document.

Certainly, this would give for a poor user experience and make the whole point of <img> — to load an image as quickly as possible as soon as it's encountered in the HTML (unless its lazily loaded) — absolutely useless.

With units such as px or vw, the browser can immediately resolve the length values based on what it already knows about the current environment.

For instance, vw is allowed in the sizes attribute because in resolving a value such as 80vw, the browser only needs to multiply the value 0.8 with the viewport's width, which it already knows before even the underlying HTML page is requested itself.

There's always some intuition behind design decisions for technology.

It's time for an example.

Example of the w descriptor and sizes

Unlike the case with the density descriptor (x) as we saw above, we don't need to make up a whole new example in order to demonstrate the width descriptor (w), for we already have one.

In the previous section, we showed an example of a banner image whereby the browser was choosing a very high-res image from srcset even though there was absolutely no need to, given the area that image was meant to fill.

The thing is that density descriptors don't provide enough context to the browser to make an educated decision in such a case, where the image's size is superbly huge to cater to many devices at once.

Following we review the <img> element's markup:

<img srcset="banner-1500.png 1x, banner-3000.png 2x" style="width: 100%">

Let's now make this much better, simply by leveraging the w descriptor.

We already know that at all times, the image must consume the entire width available to it. This goes well with the vw unit which works in terms of the viewport's width. In our case, we need the value 100vw.

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 1500px wide whereas the latter is 3000px wide. Likewise, the width descriptor for the first image source will simply be 1500w while that of the second one will be 3000w.

Figuring out the width 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:

<img sizes="100w" srcset="banner-1500.png 1500w, banner-3000.png 3000w" style="width: 100%">

Let's see what the browser has to show to us for this <img>:

Live Example

This time, no matter what our zoom factor is, the image rendered is always the 1500px version, banner-1500.png:

The output with a device pixel ratio of 2, using the w descriptor
The output with a device pixel ratio of 2, using the w descriptor

This makes perfect sense because regardless of the zoom factor, the screen is still the same and is better off at just representing the 1500px image, at all times.

Should we use x or w in srcset?

Now this is a very good question. After all, it can get confusing whether we should opt for the simpler x descriptor in srcset or the more powerful w descriptor.

So should we use x or w in srcset?

Well, there isn't a right or wrong choice here. But we can use some helpers to make our decision-making process a little bit simpler.

Below we lay out these helpers, detailing when to use which descriptor:

Density descriptor (x)Width descriptor (w)
  • When the image has a small width (e.g. 300px, 700px)
  • When the image's size is the same
  • When a very simple way is needed without much, if any, complexity
  • When the image has a considerably large width (e.g. 1500px)
  • When the image's size changes with the viewport's width
  • When more control is desired at the cost of more complexity

Further reading

Woah! This chapter was one serious learning span.

Frankly speaking, the topic of srcset is quite an extensive one to cover.

Many resources out there merely skim through srcset on the outskirts, not addressing every single aspect related to srcset — things such as device pixels, CSS pixels, DPR, intuition behind width descriptors, and so on.

At this point, you're hopefully well-versed with what srcset is meant for and how exactly should you use it.

Still, if you feel you need to learn even more, there are a couple of resources listed below to help you explore more of what we explored in this chapter or at least review everything we covered here but from someone else's perspective.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage