Responsive Images in CSS

The term "responsive images" has come to mean "responsive images in HTML", in other words, the srcset and sizes attribute for <img> and the <picture> element. But how do the capabilities that these things provide map to CSS?

CSS generally wasn't really involved in the responsive images journey of the last few years. That's for good reason: CSS already has the tools. Responsive images was, in a sense, just catching up to what CSS could already do. Let's take a look.

srcset in CSS

In HTML, srcset is like this (taken from the Picturefill site):

<img srcset="
  examples/images/image-384.jpg 1x, 
  examples/images/image-768.jpg 2x
" alt="…">

One image for 1x displays, a larger image for 2x displays. If we wanted to do that same thing, only as a background-image in CSS, we could do:

.img {
  background-image: url(examples/images/image-384.jpg); 
}
@media 
  (-webkit-min-device-pixel-ratio: 2), 
  (min-resolution: 192dpi) { 
  .img {
    background-image: url(examples/images/image-768.jpg);
  }
}

There is a difference here, though. As I understand it, the way srcset is spec'd out is a bit like a suggestion. The attribute and value provide information about what is available and the browser decides what is best at the moment. Or at least, it could, if browsers chose to implement it that way. With a @media query, what is declared shall be.

Resolution media queries are fairly well supported:

Browser support for resolution media queries

There is another way as well, that is actually closer to how srcset works, and that's using the image-set() function in CSS:

.img {
  background-image: url(examples/images/image-384.jpg);
  background-image: 
    -webkit-image-set(
      "examples/images/image-384.jpg" 1x,
      "examples/images/image-768.jpg" 2x,
    );
  background-image: 
    image-set(
      "examples/images/image-384.jpg" 1x,
      "examples/images/image-768.jpg" 2x,
    );
}

It has a little less support than resolution queries:

Browser support for image-set()

It's much closer to srcset, not only because the syntax is similar, but because it allows for the browser to have a say. According to the (still in draft) spec:

The image-set() function allows an author to ignore most of these issues, simply providing multiple resolutions of an image and letting the UA decide which is most appropriate in a given situation.

There is no perfect 1:1 replacement for srcset in CSS, but this is pretty close.

sizes in CSS

The sizes attribute in HTML is very directly related to CSS. In fact, it basically says: "This is how I intend to size this image in CSS, I'm just letting you know right now because you might need this information right this second and cannot wait for CSS to download first."

Sample:

<img
  sizes="(min-width: 40em) 80vw, 100vw"
  srcset=" ... "
  alt="…">

Which assumes something like this in the CSS:

img {
  width: 100%;
}
@media (min-width: 40em) {
  /* Probably some parent element that limits the img width */
  main {
    width: 80%;
  }
}

But sizes alone doesn't do anything. You pair it with srcset, which provides known widths, so the browser can make a choice. Let's assume just a pair of images like:

<img
  sizes="(min-width: 400px) 80vw, 100vw"
  srcset="examples/images/small.jpg 375w,
          examples/images/big.jpg 1500w"
  alt="…">

The information in the markup above gives the browser what it needs to figure out the best image for it. The browser knows 1) it's own viewport size and 2) it's own pixel density.

Perhaps the browser viewport is 320px wide and it's a 1x display. It now also knows it will be displaying this image at 100vw. So it has to pick between the two images provided. It does some math.

375 (size of image #1) / 320 (pixels available to show image) = 1.17
1500 (size of image #2) / 320 (pixels available to show image) = 4.69

1.17 is closer to 1 (it's a 1x display), so the 375w image wins. It'll try to not go under, so 1.3 would beat 0.99, as far as I understand it.

Now say it was a 2x display. That doubles the amount of pixels needed to show the images, so the math is:

375 / 640 = 0.59
1500 / 640 = 2.34

2.34 wins here, and it'll show the 1500w image. How about a 1x display with a 1200px viewport?

375 / (80% of 1200) = 0.39
1500 / (80% of 1200) = 1.56

The 1500w image wins here.

This is kinda weird and tricky to write out in CSS. If we just think about 1x displays, we end up with logic like...

  • If the viewport is less than 375px, use the 375w image.
  • If the viewport is larger than 375px but less than 400px, use the 1500w image (because otherwise we'd be scaling up).
  • At 400px, the image moves to 80vw wide, so it's safe to use the 375w image for a littttttle bit ( (between 400px and 468px)
  • Over 468px, use the 1500w image.

Which we could write like:

img {
  background-image: url(small.jpg);
}
/* Only override this if one of the conditions for the 1500w image is met */
@media 
  (min-width: 375px) and (max-width: 400px),
  (min-width: 468px) {
  main {
    background-image: url(large.jpg);
  }
}

In this exact case, a 2x display, even at a really narrow width like 300px, still requires 600px make that 1.0 minimum quality, so we'd also add that to the logic:

.img {
  background-image: url(small.jpg);
}
/* Only override this if one of the conditions for the 1500w image is met */
@media 
  (min-width: 375px) and (max-width: 400px),
  (min-width: 468px),
  (-webkit-min-device-pixel-ratio: 2), 
  (min-resolution: 192dpi) {
  .img {
    background-image: url(large.jpg);
  }
}

The complexity of this skyrockets the more breakpoints (sizes) and the more provided images. And it's still not a perfect match for what responsive images (in HTML) can do, since it doesn't allow for browser discretion (e.g. the potential for a browser to consider other factors [i.e. bandwidth] to choose an image).

picture in CSS

An example:

<picture>
  <source srcset="extralarge.jpg" media="(min-width: 1000px)">
  <source srcset="large.jpg" media="(min-width: 800px)">
  <img srcset="medium.jpg" alt="…">
</picture>

This kind of thing is a fairly straight-forward conversion to media queries. The exact media queries are right there to copy:

.img {
  background-image: url(medium.jpg);
}
@media (min-width: 800px) {
  .img {
    background-image: url(large.jpg);
  }
}
@media (min-width: 1000px) {
  .img {
    background-image: url(extralarge.jpg);
  }
}

No surprise, this can get more complicated, because srcset can do it's thing within the picture element as well:

<picture>
  <source srcset="large.jpg, extralarge.jpg 2x" media="(min-width: 800px)">
  <img srcset="small.jpg, medium.jpg 2x" alt="…">
</picture>

Which translates to:

.img {
  background-image: url(small.jpg);
}
@media
  (-webkit-min-device-pixel-ratio: 2), 
  (min-resolution: 192dpi) {
  .img {
    background-image: url(medium.jpg);
  }
}
@media
  (min-width: 800px) {
  .img {
    background-image: url(large.jpg);
  }
}
@media
  (-webkit-min-device-pixel-ratio: 2) and (min-width: 800px), 
  (min-resolution: 192dpi) and (min-width: 800px) {
  .img {
    background-image: url(extralarge.jpg);
  }
}

Again, this is just waiting to blow up in complexity as you add a few more images and conditions. Slightly better with image-set():

.img {
  background-image: url(small.jpg);
  background-image: 
    -webkit-image-set(
      "small.jpg" 1x,
      "medium.jpg" 2x,
    );
  background-image: 
    image-set(
      "small.jpg" 1x,
      "medium.jpg" 2x,
    );
}
@media
  (min-width: 800px) {
  .img {
    background-image: url(large.jpg);
    background-image: 
      -webkit-image-set(
        "large.jpg" 1x,
        "extralarge.jpg" 2x,
      );
    background-image: 
      image-set(
        "large.jpg" 1x,
        "extralarge.jpg" 2x,
      );
  }
}

Could mixins help?

Probably? I'd love to see a Sass @mixin that would take all these params, factor in the image sizes (that it figures out itself by hitting the disk), and spits out some quality responsive-images-in-CSS code. Maybe there is even a way to combine resolution queries and image-set() syntaxes?

Are we actually doing this?

I'm curious how many people are actively handling responsive images in CSS. Perhaps even in a middle-ground kinda way of just swapping out larger images at large breakpoints? I wonder if, because picture/srcset is often automated, that actually has a higher adoption rate than responsive images in CSS does?