How To Use Responsive Images

Based on Andreas Bovens’ article Responsive Images: Use Cases and Documented Code Snippets to Get You Started we will look at four use cases for responsive images:

  • Change image sizes based on responsive design rules (the sizes use case)
  • Optimize for high-dpi screens (the dpi use case)
  • Serve images in different image formats to browsers that support them (the mime use case)
  • Serve different art depending on certain contextual factors (the art direction use case)

We’ll look at some example native responsive images and how they apply to these use cases.

Viewport Size Based

The most basic case is to provide different images based on the width of the viewport.

The idea is to not load huge images for devices that don’t need or want them. Since we’re only worried about sizes we can use srcset to indicate the sizes that we want to use.

<img
    srcset="home-300.jpg 300w,
            home-400.jpg 400w,
            home-800.jpg 800w,
            home-1200.jpg 1200w">
    src="opera-400.jpg" alt="My House"

DPR based

We can also use responsive images to handle images that will look good regardless of the DPR factor of the device.

Some modern displays have up to 5x resolution so the standard image can range from 1×1 to o5x5 pixels per logical pixel

<img
  src="museum-outside-1x.jpg"
    alt="The Oslo Opera House"
  srcset= "museum-outside-2x.jpg 2x,
           museum-outside-3x.jpg 3x,
           museum-outside-4x.jpg 4x,
           museum-outside-5x.jpg 5x">

Media Query-like conditions

<img
  sizes="(max-width: 30em) 100vw,
         (max-width: 50em) 50vw,
         calc(33vw - 100px)"
  srcset="swing-200.jpg 200w,
          swing-400.jpg 400w,
          swing-800.jpg 800w,
          swing-1600.jpg 1600w"/>

When you use the srcset and sizes attributes on an <img /> element, you are providing information that the browser can use to make an informed decision about what image is appropriate for the user based on factors you as a developer won’t see or won’t care about.

If we provide browsers with information via srcset and sizes then browsers can make smarter decisions about the appropriate image source.

But none of that is possible when you use the &lt;picture&gt; element and its media attributes:

<picture>
  <source
    srcset="large.jpg"
    media="(min-width: 45em)"></source>
  <source
    srcset="med.jpg"
    media="(min-width: 32em)"></source>
  <img
    src="small.jpg"
    alt="The president giving an award." />
</picture>

When you use the picture element you’re telling the browser that it must use the first element where the media matches or the default element if none do.

The order of sources matters and you have to be sure that you have enough media conditions to cover all your cases and a good default.

Different Images for Portrait or Landscape

There are times when we may want to use different images for portrait and landscape modes.

This picture element will check for the device orientation and use the appropriate source material.

We could further refine this with viewport or DRP elements in the srcset attribute.

<picture>
  <source
    media="(orientation: portrait)"
    srcset="portrait.jpg"></source>
  <source
    media="(orientation: landscape)"
    srcset="med.jpg"
    ></source>
  <img
    src="portrait.jpg"
    alt="The president giving an award." />
</picture>

Different Formats for Different Browsers

Before WebP became widely supported (IE and Safari are the only browsers that don’t support it) we had to make sure that we served the right images for the right browsers.

In this picture element, both sources match the media so the browser will take the first source in document order and use that one if the browser supports the format and the second one if it doesn’t.

Finally, if it doesn’t understand the picture element, the browser will ignore it altogether and use the image element inside the picture.

<picture>
    <source
    media="(min-width: 1280px)"
    sizes="50vw"
    srcset="museum-fullshot-200.webp 200w,
      museum-fullshot-400.webp 400w,
      museum-fullshot-800.webp 800w,
      museum-fullshot-1200.webp 1200w"
        type="image/webp">
    <source
    media="(min-width: 1280px)"
    sizes="50vw"
    srcset="museum-fullshot-200.jpg 200w,
      museum-fullshot-400.jpg 400w,
      museum-fullshot-800.jpg 800w,
      museum-fullshot-1200.jpg 1200w">
    <img
    src="museum-closeup-400.jpg"
    alt="Museum Closeup"
    sizes="(min-width: 640px) 60vw, 100vw"
    srcset="museum-closeup-200.jpg 200w,
      museum-closeup-400.jpg 400w,
      museum-closeup-800.jpg 800w,
      museum-closeup-1200.jpg 1200w">
</picture>

Combining different techniques

This is a more extreme example of combining different techniques. We use multiple sources and multiple formats to make sure we reach as wide a userbase as possible.

<picture>
    <source
    media="(min-width: 1280px)"
    sizes="50vw"
    srcset="museum-fullshot-200.webp 200w,
      museum-fullshot-400.webp 400w,
      museum-fullshot-800.webp 800w,
      museum-fullshot-1200.webp 1200w"
        type="image/webp" />
    <source
    media="(min-width: 1280px)"
    sizes="50vw"
    srcset="museum-fullshot-200.jpg 200w,
      museum-fullshot-400.jpg 400w,
      museum-fullshot-800.jpg 800w,
      museum-fullshot-1200.jpg 1200w" />
    <source
    sizes="(min-width: 640px) 60vw, 100vw"
    srcset="museum-closeup-200.webp 200w,
      museum-closeup-400.webp 400w,
      museum-closeup-800.webp 800w,
      museum-closeup-1200.webp 1200w"
    type="image/webp" />
    <source
    sizes="(min-width: 640px) 60vw, 100vw"
    srcset="museum-closeup-200.jpg 200w,
      museum-closeup-400.jpg 400w,
      museum-closeup-800.jpg 800w,
      museum-closeup-1200.jpg 1200w" />
    <img
    src="museum-closeup-400.jpg"
    alt="Museum Closeup"
    sizes="(min-width: 640px) 60vw, 100vw"
    srcset="museum-closeup-200.jpg 200w,
      museum-closeup-400.jpg 400w,
      museum-closeup-800.jpg 800w,
      museum-closeup-1200.jpg 1200w">
</picture>

We have a lot of options regarding responsive images. This barely scratches the surface of leveraging responsive images but it’s a good start and a good tool to add to the arsenal.

Links and resources

Concepts and examples of responsive images

In working through automating image compression and responsive image generation I wanted to add more areas to the conversation about responsive images without cluttering the post about automation.

How should we get our source images?

The first thing to consider is what should be the ideal format for our source images. The Gulp task with manipulates the size of the images so it pays to have the largest possible image available to process. It’s always better to shrink an image than it is to enlarge it, besides you will compress the images later anyways.

There are ways to use Machine Learning to improve the quality of existing images even if we don’t have access to the originals.

Device Pixels versus Logical Pixels

We need to look at the difference between logical pixels and device pixels since this will affect the way we generate our responsive images and what types of images we want to use for our originals.

Let’s assume that we have an iPhone X with a resolution of 2436 x 1125 device pixels and Device Pixel Ratio (DPR) of 3.

The DPR is defined by the device manufacturer. Simply put, it refers to the number of physical pixels contained in one logical pixel. For example, a device with a DPR of 2 means that one logical pixel contains 4 (2 x 2) physical pixels. Similarly, a DPR of 3 implies that a single logical pixel is equivalent to 9 physical pixels.
from A Guide to Responsive Images on the Web

If we divide the height and width by the DPR We get the logical size of the screen in this case: 812 x 375 and that’s why the following CSS would work:

.example {
  width: 400px;
}

We’re telling the CSS parser to use 400 logical pixels instead of 400 device pixels.

Native Responsive Images

There are two ways to create native responsive images: the picture element and the srcset and sizes attributes. We’ll look at how each of these methods works and then we’ll look at the use cases.

Picture

Using picture and source elements in your page is similar to the video element. You use one or more source elements to indicate the different images that we want to use.

We can leverage the picture element to work with WebP in browsers that can support those formats.

The following code example gives us the following:

  1. If the width of the screen is 40em and the browser supports WebP use this source and the corresponding image based on device’s DPR
  2. If the width of the screen is 40ems and the browser does not support WebP use this source and the corresponding image based on device’s DPR
  3. If neither 1 and 2 are met uses this source and the corresponding image based on device’s DPR
  4. if the browser doesn’t understand the source attribute then use the img element. This must be the last children of a picture element
<picture>
  <!--1-->
  <source media="(min-width: 40em)"
    srcset="big.webp 1x, big-hd.webp 2x">
  <!--2-->
  <source media="(min-width: 40em)"
    srcset="big.jpg 1x, big-hd.jpg 2x">
  <!--3-->
  <source
    srcset="small.jpg 1x, small-hd.jpg 2x">
  <!--4-->
  <img src="fallback.jpg" alt="">
</picture>

srcset and sizes

srcset defines the set of images we want the browser to choose between and the DPR for each image. The browser will select the best image to use based on the device’s characteristics.

The 2x, 3x and 4x values indicate different DPR values. Where there is no value it is assumed to be 1x.

<img
  srcset= 'two.png 2x,
           three.png 3x,
           four.png 4x'
  src="small.jpg" alt="A rad wolf">

We can also add a width attribute instead of the DPR indicator. To assign the width of the image, we add a w to the pixel width number for the image.

The values for w can be almost any length value, e.g. em, rem, pixels, and viewport width, except percentages. The vw value is recommended as an alternative if a relative value is needed.

Only when we use width values we can add the sizes attribute.

The sizes attribute gives a set of media query-like conditions and width of images to use if the condition is met.

A media condition is not exactly a media query. It is part of a media query. It doesn’t allow us to specify media types, e.g. screen or print, but accepts the condition we usually add to a media type.

A valid media condition can be either –

  • A plain media condition, e.g. (min-width: 900px)
  • A “not” media condition, e.g. (not (orientation: landscape))
    An “and” media condition, e.g. (orientation: landscape) and (min-width: 900px)
  • An “or” media condition, e.g. ((orientation: portrait) or (max-width: 500px))

From: Responsive Images – The srcset and sizes Attributes

As I understand it the idea is that the browser will check the media conditions in the sizes attribute and, based on what condition is used, the browser will select the image to used from those available in the srcset.

The full example using srcset and sizes

<img
  srcset= "large.jpg 1024w,
          medium.jpg 640w,
          small.jpg 320w"
  sizes=  "(min-width: 48em) 33.3vw,
          (max-width: 25em) 75vw,
          100vw"
  src="small.jpg" alt="A rad wolf">

Automating image work

Since we are unlikely to compress images and less likely to create the individual images necessary to do a good job with responsive images, we have to leverage technologies to do it for us.

I use Gulp to build sites so I’ve incorporated image processing into my development and production pipeline.

There are two tasks for working with images in Gulp. I will cover them separately and explain the decisions I’ve made about them.

Install and configure the plugins

All these plugins are installed with Node. Like we do in all Node projects we first initialize an empty package.json. The --yes flag will accept all default parameters.

npm init --yes

We then install the packages that we’ll use in the tasks.

npm i -D gulp \
gulp-imagemin \
imagemin-mozjpeg \
imagemin-webp \
gulp-responsive

Once we have initialized our package.json and installed all the packages that we want to use, it’s time to start work in the Gulp configuration

The rest of the code in this post lives in a gulpfile.js file.

At the very top, we import Gulp and the plugins we want to use using the CommonJS require syntax.

// Imagemin and Plugins
const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const mozjpeg = require('imagemin-mozjpeg');
const webp = require('imagemin-webp');
const responsive = require('gulp-responsive');

Process Images

The first function is to compress images using Imagemin and the gulp-imagemin plugin.

This will handle SVG, GIF, PNG, JPG and WebP images.

function processImages() {
  return gulp.src('src/images/originals/**')
  .pipe(imagemin([
    imagemin.gifsicle({interlaced: true}),
    imagemin.optipng({optimizationLevel: 5}),
      imagemin.svgo({
          plugins: [
              {removeViewBox: true},
              {cleanupIDs: false}
      ]}),
    use: [
      mozjpeg(),
      webp({quality: 85})],
  ]))
  .pipe(gulp.dest('src/images'))
}

Although not strictly necessary, I chose to be explicit as to what plugins I’m using and what parameters each of these plugins uses.

Gifsicle, Optipng, and SVGO are bundled with Imagemin so we can just use them as-is. Mozjpeg and WebP are additional libraries that add functionality in addition to or instead of the default bundled libraries.

Generate Responsive Images

The next step is to generate a set of responsive images.

The idea is that for each JPG and PNG image we will generate a set of responsive images. I’ve reduced the code example below to a single size with 2 different resolutions.

Device Pixel versus Logical Pixel

Before we jump into the code we need to look at the difference between logical pixels and device pixels since this will affect the way we generate our responsive images and what types of images we want to use for our originals.

Let’s assume that we have an iPhone X with a resolution of 2436 x 1125 device pixels and Device Pixel Ratio (DPR) of 3.

The DPR is defined by the device manufacturer. Simply put, it refers to the number of physical pixels contained in one logical pixel. For example, a device with a DPR of 2 means that one logical pixel contains 4 (2 x 2) physical pixels. Similarly, a DPR of 3 implies that a single logical pixel is equivalent to 9 physical pixels.
from A Guide to Responsive Images on the Web

If we divide the height and width by the DPR We get the logical size of the screen in this case: 812 x 375 and that’s why the following CSS would work:

.example {
  width: 400px;
}

We’re telling the CSS parser to use 400 logical pixels instead of 400 device pixels.

The code

The code follows standard Gulp practices.

We first select the src for the task. In this case, we’ve chosen all the JPG and PNG files under src/images/hires.

We pipe the input through gulp-responsive to generate multiple versions of each image.

I’ve chosen to demonstrate one image into DPR resolutions and three formats, JPEG, PNG, and WebP.

The @2x images are using double the number of pixels to accommodate higher DPR values. When/if we wanted to add a 3x image we can add it like we did the 2x image in the examples below.

function generateResponsive() {
return gulp.src([
    'src/images/hires/**/*.{jpg,png}',
  ])
  .pipe(gulp.responsive({
    '*': [{
    // image-small.jpg is 200 pixels wide
      width: 200,
      rename: {
        suffix: '-small',
        extname: '.jpg',
      },
    }, {
    // [email protected] is 400 pixels wide
      width: 200 * 2,
      rename: {
        suffix: '[email protected]',
        extname: '.jpg',
      },
    }, {
    // image-small.png is 200 pixels wide
      width: 200,
      rename: {
        suffix: '-small',
        extname: '.png',
      },
    }, {
    // [email protected] is 400 pixels wide
      width: 200 * 2,
      rename: {
        suffix: '[email protected]',
        extname: '.png',
      },
    }, {
    // image-small.webp is 200 pixels wide
      width: 200,
      rename: {
        suffix: '-small',
        extname: '.webp',
      },
    }, {
    // [email protected] is 400 pixels wide
      width: 200 * 2,
      rename: {
        suffix: '[email protected]',
        extname: '.webp',
      },
    }, {
    // Global configuration for all images
    // The output quality for JPEG, WebP
    // and TIFF output formats
      quality: 80,
      // Use progressive (interlace) scan
      // for JPEG and PNG output
      progressive: true,
      // Skip enalrgement warnings
      skipOnEnlargement: true,
      // Strip all metadata
      withMetadata: true,
    }],
  })
      .pipe(gulp.dest('dist/images')));
}

We might not need all the images but it’s better to have them and no need them than need them and not have them.

Export the functions

The final step is to export the functions soo that Gulp can see them. It’s no different than exporting functions in ES2015 modules.

exports.imagemin = processImages;
exports.responsive = generateResponsive;

This will enable us to run them as gulp imagemin and gulp-responsive.

Loading scripts the right way for everyone

Differential loading is the technique where you load different content for different browsers that support different sets of Javascript features and APIs.

<script type="module" src="/js/modern.mjs"></script>
<script nomodule defer src="/js/legacy.js"></script>

This works awesome with modern browsers that understand type="module" and that will happily ignore nomodule.

The problem is that we can’t make that assumption safely. Some browsers will download the nomodule script twice and others that will download both scripts, even when they will only execute one of them.

Jeremy Wagner’s article A Less Risky Differential Serving Pattern proposes the following hack to make sure that all browsers will load a single version of the code for the page depending on whether they use modules or not.

<script>
  // Create a new script element 
  //to slot into the DOM.
  var scriptEl = document.createElement("script");

  // Check whether the script element
  // supports the `nomodule` attribute.
  if ("noModule" in scriptEl) {
    scriptEl.src = "/js/modern.mjs";
    scriptEl.type = "module";
  } else {
    scriptEl.src = "/js/legacy.js";
    scriptEl.defer = true;
  }

  document.body.appendChild(scriptEl);
</script>

In a separate article in the 2018 Performance Calendar entry Doing Differential Serving in 2019 he goes more in-depth on how to prepare the bundles that will differentially serve.

Saving preferences in the user’s browser

In working on another idea (how to allow users to select their preferred reading configuration on a web page) I came across another issue: How do we allow users to save their preferences so they don’t have to redo their work every time they visit the content.

I remembered an old project that I worked on to test the same hypothesis from a different point of view.

Rather than use that project, I’ve created the full project in Github. I will only highlight details that I think are important.

The idea is that after the user changes the settings, the browser will save them to local storage and then when the user returns, it will use the values from local storage as the new values for the font parameters.

I hear the complaint that this will have to be done for each browser the user works with… That’s true but I’m ok with it, I don’t expect every single display to look the same and I don’t expect readers to do this on their own unless they need to.

Now that we’ve set the parameters and objectives, let’s look at the code.

The first part is an HTML box with multiple sliders, one for each attribute that we want to change. The example below only handles the weight of the font. I’ve deliberately chosen to use one decimal place for the value as I’m not certain readers would be able to tell the difference.

<div class="settings">
  <fieldset>
    <legend>Font Settings: Roboto</legend>

    <label for="robotoWeight">Weight</label>
    <input  type="range"
            id="robotoWeight"
            name="robotoWeight"
            min="400" max="900"
            value="400" step="0.1">
    <p><strong>Weight</strong>:
    <span class="weightSlider"></span></p>
  </fieldset>
</div>

In the CSS section, we take advantage of CSS variables and the fact that they are “live”, if we change the value of a variable it will automatically reflect on the page.

We import the font that we will use in the project, Roboto Variable.

We then set up our variables in the :root pseudo-element. :root is similar to the html element but it has higher specificity.

The final part of the CSS block is to use the variables using the var() function. We’re still using font-variation-settings to make sure the code works in as many browsers as possible.

@font-face {
  font-family: Roboto;
  src: url("fonts/Roboto-min-VF.woff2");
}

/* Defaults */
:root {
  --line-height: 1;
  --font-weight: "wght" 100;
  --font-width: "wdth" 100;
  font-family: Roboto, sans-serif;
  font-size: 100%;
}

.content {
  line-height: var(--line-height);
  font-variation-settings:
    var(--font-weight),
    var(--font-width);
}

The Javascript block is where the magic happens.

First, we define two functions.

The first one tests if we support local storage by creating and removing an item inside and return true if the activity succeeds.

If we cannot set or remove an item the code will fail, log a message to console and return false.

We use a try/catch block to ensure that we can return from each branch and that both success and failure will be handled appropriately.

function hasLocalStorage() {
  try {
    localStorage.setItem(mod, mod);
    localStorage.removeItem(mod);
    return true;
  } catch (e) {
    console.log('Local Storage Not Supported');
    return false;
  }
}

The second function is a convenience function to insert rules into the :root pseudo-class in the base stylesheet.

It first captures a reference to the :root CSS rule in our stylesheet.

Then we build the CSS variable by setting a property in our stylesheet rule. We add the two dashes (--) required for CSS variables and the name, with the value as the second parameter.

function setRootVar(name, value) {
  let rootStyles = document.styleSheets[0].cssRules[1].style;
  rootStyles.setProperty('--' + name, value);
}

We use the oninput handler to tell the browser what to do when the content of the input element changes.

In this case, we call setRootVar to set the font-weight CSS variable using the string "wdth" and the value of the slider as the second parameter. I decided to go the extra mile so it would be easier to build the variable and use it when we update the font-variation-settings CSS.

I’ve also stored two elements in local storage:

One is the full value of font-weight: the string and the value of the slider.

The other one is just the value of the weight slider. I’ve done this to make it easier on myself when retrieving the data later.

weight.oninput = function() {
  weightSlider.innerHTML = weight.value;
  // setting the style
  setRootVar('font-weight', ' "wght" ' + weight.value);
  localStorage.setItem('font-weight', ' "wght" ' + weight.value);
  localStorage.setItem('weight-value', weight.value);
};

The last block is a DOMContentLoaded event handler to retrieve the settings from localStorage, set the font attributes accordingly and provide defaults if the attribute is not stored in local storage or is empty.

If the attribute exists and is not empty then we update the position of the slider and the value it reflects. This way the user will not have to redo the sliders with the values they wanted (and that are reflected in the text).

If the value is not set or is null, we provide defaults that match the values we set in the CSS stylesheet.

window.addEventListener('DOMContentLoaded', event => {
  if (localStorage.getItem('font-weight') &&
    localStorage.getItem('font-weight') !== null) {
    setRootVar('font-weight', localStorage.getItem('font-weight'));
    robotoWeight.setAttribute(
      'value',
      localStorage.getItem('weight-value'),
    );
    weightSlider.innerHTML = localStorage.getItem('weight-value');
  } else {
    setRootVar('font-weight', 400);
    robotoWeight.setAttribute(
      'value',
      localStorage.getItem(400),
    );
    weightSlider.innerHTML = 400;
  }
});

There are some things I’m still working on. Some times the values do not load properly and I’m trying to figure out why.

This is the first step in providing a way to save settings for an app. We might want to expand the test to something closer to a full-blown reading application.