Really responsive images

2015-07-29

We want website images that:

  1. resize keeping aspect ration at different widths
  2. can swap out different images for phone/tablet/desktop
  3. avoid downloading tablet/desktop images on phone
  4. use high-res images when appropriate?


Standard image

<img src="tablet.png" alt="" />

The image above remains at it's normal size even if the screen gets too small. Make the screen less wide and see that the right edge of the image is "cut-off" and causes horizontal scrolling



Fix #1: resize keeping aspect ratio at different widths

<img class="fix1" src="tablet.png" alt="" />

The basic fix has been applied to the above image. Notice that when you resize the browser window to be less wide, the image scales with it. Keeping the aspect ration intact.

.fix1{
    max-width: 100%;
    height: auto;
}
This code uses the max-width: 100%; setting to ensure images never go beyond the width of their parent container. If the parent container shrinks below the width of the image, the image will scale down along with it. The height: auto; setting ensures the images' aspect ratio is preserved as this occurs.


Fix #2 & #3: different images at sizes without extra downloading

Now it gets interesting. Instead of the usual IMG tag, we use the html5 PICTURE tag.

<picture>
    <source srcset="phone.png" media="(max-width: 768px)">
    <source srcset="desktop.png">
    <img srcset="downloadDefault.png" alt="This picture loads on non-supporting browsers.">
</picture>

So if you load this demo page from a phone, you won't download the desktop images.

Note: While testing this, you don't want to start in desktop and shrink the browser down. That will get you both pictures because it already detected you at a larger size. Also, you might need to clear the cache once you are in phone testing size. (Using Chrome, when you are in DevTools(F12) the refresh button gains new abilities. Click and hold on it to see options to "Empty cache and reload")

I started using the IMG tag with the srcset attibute, but I didn't like the lack of control. With this method, the browser picks the best img source for a user's pixel density and the size of the image. The results would vary because similar sized devices have different pixel densities. (iPhone6 = 2x vs. iPhone6+ = 3x vs. Samsung Galaxy S3 = 2x)

Using the PICTURE tag makes it more obvious what and when different images are used. This is called the "art direction" approach. In all my projects, the hero image needs to be slightly different for the overlayed text to stay readable. I need differently cropped versions of an image to match my text which is wrapping or resizing.

Let's look at an example which has a simple phone/tablet/desktop image swap:

<picture>
    <source srcset="phone.jpg" media="(min-width: 0px) and (max-width: 767px)" />
    <source srcset="tablet.png" media="(min-width: 768px) and (max-width: 991px)" />
    <source srcset="desktop.jpg" />
    <img srcset="downloadDefault.png" alt="" />
</picture>

To make things scale, I'm applying the scaling fix (#1 above) to the IMG in the PICTURE with some css.

picture img{
  max-width: 100%;
  height: auto;
}

You could also do the same thing by using the Bootstrap img-responsive class on the img:

<picture>
    <source srcset="phone.png" media="(min-width: 0px) and (max-width: 767px)" />
    <source srcset="tablet.png" media="(min-width: 768px) and (max-width: 991px)" />
    <source srcset="desktop.png" media="(min-width: 992px)" />
    <img srcset="downloadDefault.png" alt="" class="img-responsive" />
</picture>

Note: You only need to style the IMG tag. Not the SOURCE tags. Those basically only modify the IMG tag, which is why you need to have one in there.

I left the (min-width: 0px) part in the media query to make it more clear what's going on. You start with your phone image and apply the media query to say "when" it applies. It sorta cascades down like css. If the two sources on top don't match their media query requirements the next one wins.



Fix #4: Retina and pixel density

I tend to be conservative when it comes to using higher res images for devices. Some phones and tablets work wonderfully with them...if they are on wifi. Otherwise it's slowing down the experience. So I tend to save the 2x pics for desktops.

Pixel density is where we change the philosophy - we allow the browser to make the call. We just provide the optional high-res images. Update: I hear that the browser can detect lower bandwidth and make the decision to load a lower res image. That's amazing and worth experimenting with.

So let's look at an example which swaps in a 2x desktop image if our browser thinks we can handle it.

Note 1: If you download the high-res image once, you will need to clear your cache (as mentioned near the top) to see the lower-res images load.

Note 2: If your high-res image isn't really high-res (in Photoshop look at Image - Image Size - Resolution) your image may appear smaller on the screen than it should. This drove me nuts for awhile.

<picture>
    <source srcset="phone1x.png" media="(min-width: 0px) and (max-width: 767px)" />
    <source srcset="tablet1x.png" media="(min-width: 768px) and (max-width: 991px)" />
    <source srcset="desktop1x.png, img/desktop2x.png 2x" media="(min-width: 992px)" />
    <img srcset="downloadDefault.png" alt="" />
</picture>

Notice that a comma separates the different res images. Also the '2x' after an image to provide it's suggested usage for the browser. The 2x is the 'display density' and if a value isn't provided, the default is 1x. You can change that number to whatever you think is needed.

Traps! Cache can work against you if you arent careful. If you happen to load your 2x image somehow and it's loaded into the device's cache, it will pick that version to use because it's already downloaded. Usually that's fine, but it could cause problems if your high-res image is cropped differently. Keep different "art-direction" controlled by separate source/media-attributes.



What about browser support?

At the time of this writing, IE and Safari(including iOS Safari) need a polyfill. [more]

The excellent solution is Picturefill!

Btw: On their site, you might get confused by examples if you don't recognize the different between approaches which use IMG vs. PICTURE.

Since, I'm using the PICTURE approach (for better art direction control) it requires some finessing for ie9 as well.

To support IE9, you will need to wrap a video element wrapper around the source elements in your picture tag. You can do this using conditional comments, like so:
<picture>
  <!--[if IE 9]><video style="display: none;"><![endif]-->
  <source srcset="extralarge.jpg" media="(min-width: 1000px)">
  <source srcset="large.jpg" media="(min-width: 800px)">
  <!--[if IE 9]></video><![endif]-->
  <img srcset="medium.jpg" alt="…">
</picture>