Responsive images: How they work – and how to use them with “Art Direction” and “Dark Mode”
Tags: html, css, webdev, codepen
Published: 2021-03-27 14:56:43

To create a fast and sustainable website, the top priority is to minimize the amount of assets, mainly images. But responsive images can be tricky, and the syntax complicated. This post will hopefully help you understand them better.

---

On a project I worked on recently, I had the following conversation with a content-editor:  

— “Why can't I change the image [for a component/article] for a given breakpoint?” 
— “Oh, but we *do* already scale the images server-side, and send different resolutions to the client!“
— “No, I want to change to a *completely* different image!” 
— “Ah, you mean ‘Art Direction’. Sorry … we can’t do that in the [current] solution.

Later, a designer on the same project asked me:  
— “Can I change the image [for a component/article], if the user has ‘dark mode’?”

And I replied:  
— “Ha ha! And you probably *also* want to change that image to a *completely* different image for a given breakpoint? *And* server-side-scale all the images in-between?” 

To which he replied:  
— “Yes, of course!”

So this is what the clients want — or at least what *that* particular client wants.  
Is it doable? Yes, but not in an easy, CMS-editor-friendly way.

But before we go there, let's look at some examples, and the standard way to do *responsive images,* using either the `<img>`-tag stand-alone, or within the `<picture>`-tag.

----------

## Use-case: dr.dk

One of the most popular sites in Denmark is that of the national television, [dr.dk](https://dr.dk). 
They use the `<picture>`-tag for responsive images, with multiple `<source>`-tags for the media-queries and logic.

To keep it simple, I’m going to use a *device-pixel-ratio (DPR)* set to 1. Different devices have different DPR’s (some, newer phones have a DPR of 3), but most desktop PC screens have a DPR of 1.

The first example uses this little teaser-block from dr.dk:

![drdk1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/md2gy8fgcc4ex18k2z1r.png) 

The image is displayed at `160x90px`, but I took a look at its intrinsic size.
To see which image-src is currently used, open *Dev Tools*, select the image in "Elements", then open the `console`-drawer — and type:

`$0.currentSrc`

With a DPR of 1, the intrinsic size was `336x189px` , almost double of what it needs to be.
With a DPR of 2, the size was `671x378px` - again, more than double of what it needs to be.

Here’s another example:

![drdk2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awf4l5qbzabknk1f3gk5.png) 

The image is scaled up, until the max-width of `373px` is reached. However, the intrinsic image is based on the **viewport**-width, so with a browser-window-width of `2000px`, the loaded image is actually `1080x698px`. 

This, large image is `53kb`, but could/should be just `17kb`.
If a modern format like `webp` is used, the final size is less than `13kb` — almost 1/4th of the actual size. 

The markup for the `<picture>`-tag in this particular case, is a whopping **11kb!**

That’s *almost* the same size as the largest image itself, if `webp` had been used with correct scaling. It makes you wonder whether responsive images are always necessary?

It’s the same when you use SVG’s with `<use>` - sometimes the link to the SVG is longer than the SVG-code itself.
Should we have a threshold?

If you have a hero-image which is `100%` wide, there’s no doubt: you *need* to use responsive images. But in a case like this, where the difference between the minimum and maximum display sizes is minuscule, I don’t think it makes *any* sense — especially, when the markup-size almost exceeds the image-size *before* the image has even been loaded.

----------

## `sizes` to the rescue

The key to loading images in the correct display-size is the `sizes`-attribute.

In some of the code I inspected at dr.dk, I noticed this:
`<source media="(min-width: 720px)" sizes="650px" srcset="...">`

Here, they *do* have the `sizes`-attribute, but used in a way, so it *always* returns the `650px` image, when the `min-width` is larger than `720px`, or until you add another `<source>`-tag.

However, for a simple case like this, there’s no need to use the `<picture>`- and multiple `<source>`-tags (I’ll use these for ‘art direction’ and ‘dark mode’ later):

```html
<img alt="alt text" src="https://some.cdn/img/test.webp" loading="lazy" srcset="
  https://some.cdn/img/test.webp?w=250 250w,
  https://some.cdn/img/test.webp?w=450 450w,
  https://some.cdn/img/test.webp?w=650 650w,
  https://some.cdn/img/test.webp?w=850 850w,
  https://some.cdn/img/test.webp?w=1050 1050w,
  https://some.cdn/img/test.webp?w=1250 1250w,
  https://some.cdn/img/test.webp?w=1450 1450w,
  https://some.cdn/img/test.webp?w=1650 1650w,
  https://some.cdn/img/test.webp?w=1850 1850w"
sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
(min-width: 1200px) 33vw,
100vw" />
```

This code will display the image at `100vw` by default, but when the viewport-width is between `768px` and `1199px`, a size of `50vw` will be used — and finally, when the viewport-width is larger than `1200px`, the image will be `33vw`. 

The `srcset` is rendered from an array of breakpoints: 
`[250,450,650,850,1050,1250,1450,1650,1850]` 

… and requires a server-side scaler-function, which most of the CMS-platforms I’ve worked with has (or has available as a plugin). Another option is to use an image delivery service like Cloudflare, Akamai or Imgix. These tend to have premium caching, http2/http3 out-of-the-box, and the ability to return an image in the best suitable format, based on the capabilities of the user’s browser. 

*For example:*
An editor uploads an image in the best possible quality, a 20MB jpeg-file. 
The rendered markup will point to the jpeg:

```html
https://some.cdn/img/image.jpeg?w=250 250w
```

But the *actual* image returned by the image delivery service, will be an optimised `webp`-file.

So, an `<img>` -tag with `srcset` and `sizes` configured correctly, works just fine **if the editor knows the final display-size**. How likely is that? Before I try to answer *that* question, let’s continue with ‘art direction’ and ‘dark mode’.

----------
## Art Direction: Switching images

Using the `<picture>`-tag with one or more `<source>`-tags, it’s possible to switch to *another* image when a specified *condition* is met. This is known as ‘art direction’ and can be any @media-rule, like (`min-width: 768px)` or `prefers-color-sheme: dark)`, or even `(prefers-contrast: high)`

Example, switching from `small.jpg` to `big.jpg`, when the viewport-width is minimum `768px`:

```html
    <picture>
  <source
    media="(max-width: 767px)"  
    srcset="
      https://some.cdn/img/small.jpg?w=250 250w,
      https://some.cdn/img/small.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">

  <source
    media="(min-width: 768px)"  
    srcset="
      https://some.cdn/img/big.jpg?w=250 250w,
      https://some.cdn/img/big.jpg?w=450 450w" ...etc
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">
  <img alt="alt text" src="https://some.cdn/img/small.jpg" loading="lazy" />
</picture>
```

---

> **Note:** The Codepen demos in this post will not work in *Safari* (probably because they're within `<iframe>`s). You can copy/paste the HTML and CSS to a locally hosted file, and it will work just fine. Also, since the assets are not on a server that supports scaling, the *scaler*-method is for decoration only. I recommend opening the demos in fullscreen, resize and refresh your browser.

---

{% codepen https://codepen.io/stoumann/pen/KKawZPz %}

## Dark Mode
Instead of using `media="(min-width: 768px)"` to switch to another image, we can use *another* type of media-query, `media="(prefers-color-scheme: ...)"`.

Example, switching from `light.jpg` to `dark.jpg`, depending on which color-scheme the user prefers:

```html
<picture>
  <source
    media="(prefers-color-scheme: light)"  
    srcset="
      https://some.cdn/img/light.jpg?w=250 250w,
      https://some.cdn/img/light.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">
  
  <source
    media="(prefers-color-scheme: dark)"  
    srcset="
      https://some.cdn/img/dark.jpg?w=250 250w,
      https://some.cdn/img/dark.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">
  <img alt="alt text" src="https://some.cdn/img/light.jpg" loading="lazy" />
</picture>
```

We can also *combine* these two:

```html
<picture>
  <source
    media="(max-width: 767px) and (prefers-color-scheme: light)"  
    srcset="
      https://some.cdn/img/light-small.jpg?w=250 250w,
      https://some.cdn/img/light-small.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">

  <source
    media="(min-width: 768px) and (prefers-color-scheme: light)"  
    srcset="
      https://some.cdn/img/light-large.jpg?w=250 250w,
      https://some.cdn/img/light-large.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">

  <source
    media="(max-width: 767px) and (prefers-color-scheme: dark)"  
    srcset="
      https://some.cdn/img/dark-small.jpg?w=250 250w,
      https://some.cdn/img/dark-small.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">

  <source
    media="(min-width: 768px) and (prefers-color-scheme: dark)"  
    srcset="
      https://some.cdn/img/dark-large.jpg?w=250 250w,
      https://some.cdn/img/dark-large.jpg?w=450 450w" ...etc 
    sizes="(min-width: 768px) and (max-width: 1199px) 50vw,
    (min-width: 1200px) 33vw,
  100vw">
  <img alt="alt text" src="https://some.cdn/img/light-small.jpg" loading="lazy" />
</picture>
```

---

> **Note:** To toggle between `light` and `dark`, you can either use your *OS System Settings* or *Chrome Dev Tools:*

![PrefersColor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xa3uqcauqztzy4t4a6bq.png)
 
---

Codepen demo:

{% codepen https://codepen.io/stoumann/pen/bGgNaNN %}

---

## Keeping the aspect-ratio

When switching between multiple images, there’s a risk some of these images will not have *exactly* the same height and width (unless the server/image delivery service force it).

For these cases, I use a small ‘hack’ (until native `aspect-ratio` has landed in all browsers):

```html
<picture style="--h:9;--w:16;">
```

And in CSS:

```css
picture {
  position: relative;
}
picture::before {
  content: '';
  display: block;
  padding-bottom: calc(var(--h) / var(--w) * 100%);
}
picture img {
  height: 100%;
  left: 0;
  object-fit: cover;
  position: absolute;
  top: 0;
  width: 100%;
}
```

This also gives you the flexibility to *change* the aspect ratio from CSS, if needed:

```css
@media screen and (min-width: 1200px) {
  picture {
    --h: 1;
    --w: 4;
  }
}
```

--- 

## Making it generic

Now, back to the question “Does the editor know the final display-size?”

Of course not. This should be defined in the grid/content-block-setup by devs. 

So if an editor drags "component X" into area "Y", devs should know how to display that image.

That’s all fine, but we typically tend to code functionality like this on a *per component* basis.

I’d like a **generic** *Image Component*, that can be used standalone or within other components. Through a configuration-interface, a developer (or editor!), should be able to create a unique configuration, that can be stored as JSON (or some other data-format, the server can render markup from).

I've been working on one, and the initial version is on Codepen:

{% codepen https://codepen.io/stoumann/pen/abpzVGW %}

Scroll down to the **Presets**-section, click on the various presets, and see the markup change.

It stores a configuration (preset) in JSON:

```json
{
  "description": "Simple, responsive image",
  "id": "52197ed5-d7d2-400f-b1ab-6fa9ca3ccff8",
  "values": [
    {
      "alt": "alt text",
      "aspectHeight": 0,
      "aspectWidth": 0,
      "crossorigin": "anonymous",
      "decoding": "async",
      "images": [
        {
          "breakpoint": 0,
          "colorscheme": "",
          "src": "[IMAGE-1].jpg"
        }
      ],
      "loading": "lazy",
      "path": "../assets/img/",
      "scaler": [
        250,
        450,
        650,
        850,
        1050,
        1250,
        1450,
        1650,
        1850
      ],
      "sizes": [
        {
          "breakpoint": 0,
          "size": 100,
          "unit": "vw"
        }
      ]
    }
  ]
}
```

The goal is to have a component, where a developer (or editor!) picks a configuration (preset) from a drop-down, for instance “Art Direction with Dark Mode”. This particular configuration requires four images, so the component should display four image-placeholders/fields for images.

Time will tell if some clever backend-developer can create such an interface — until then, I’ll use the tool to create the static markup I need for my components ;-)

Is the tool useful? Do you have ideas on how to improve it?

Let me know in the comments!

---

## DPR

You might have noticed, I didn’t add any DPR-related media-queries to the tool. In all my tests, I didn't have to. It seems modern browsers switch to a higher resolution image, when you use a device with a DPR greater than 1. So: if the image returned should normally (DPR1) be approx. 768px, a DPR2-device will recieve an image closer to 1536px.

--- 

## Conclusion

It’s rarely you see a site that does responsive images correctly.  
And that, unfortunately, includes most of the projects I've been working on myself. 

Why is that?
Short answer: **It's difficult!**

Hopefully, this tutorial and the tool I’ve created, will make it easier for you to deal with in your next project. 

Thanks for reading!

----------

**Further reading**
https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images
https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/
https://medium.com/@elad/a-complete-guide-for-responsive-images-b13db359c6c7

---

**Note:** I started writing this article a while ago. 
It seems dr.dk has since fixed their issues (using Akamai) with responsive images.