DEV Community

loading...
Cover image for Responsive images: How they work – and how to use them with “Art Direction” and “Dark Mode”

Responsive images: How they work – and how to use them with “Art Direction” and “Dark Mode”

madsstoumann profile image Mads Stoumann ・9 min read

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

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

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

<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" />
Enter fullscreen mode Exit fullscreen mode

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:

https://some.cdn/img/image.jpeg?w=250 250w
Enter fullscreen mode Exit fullscreen mode

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:

    <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>
Enter fullscreen mode Exit fullscreen mode

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.


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:

<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>
Enter fullscreen mode Exit fullscreen mode

We can also combine these two:

<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>
Enter fullscreen mode Exit fullscreen mode

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

PrefersColor


Codepen demo:


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

<picture style="--h:9;--w:16;">
Enter fullscreen mode Exit fullscreen mode

And in 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%;
}
Enter fullscreen mode Exit fullscreen mode

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

@media screen and (min-width: 1200px) {
  picture {
    --h: 1;
    --w: 4;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

It stores a configuration (preset) in 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"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide