Measuring Image Widths in JavaScript (Carefully!)

Let's say you want to find an <img> on the page and find out how wide it is in JavaScript. Maybe you need to make some choices based on that width (or height, or both) You can definitely do that. The DOM will even give you a variety of dimensions to choose from depending on what you need. There is definitely a catch though.

Here's the image:

<img src="image.jpg" alt="an image">

And here's us selecting it, by finding the first <img> JavaScript can find in the DOM:

var img = document.querySelector("img");

Let's just log the width:

// How wide the image is rendered at (which could be affected by styles)
console.log(img.width);

// How wide the image itself actually is
console.log(img.naturalWidth);

// There is also `offsetWidth` (width with border and padding) and `clientWidth` (width with just padding)
// Those aren't usually too useful with images

Problem alert! You're probably going to get weirdly inconsistent results with that.

The issue is rooted in when the JavaScript runs. If the JavaScript runs before that image has fully downloaded and rendered, the result will be 0. If the JavaScript runs after the image has downloaded and rendered, the result will be correct. In our case, the image we're working with is 640 (pixels wide).

It's extra confusing, because sometimes even if your code doesn't account for this, you'll still sometimes get the correct result. That's because, probably, the image is in cache and it's put onto the page so quickly that by the time your bit of JavaScript runs, it knows the proper offsetWidth.

This is a race condition, and that's a bad thing.

The Good Way

The minimum you can do is ensure the image has loaded before you measure it. Images emit a load event when they are finished, and you could use the callback to do the measurements then.

img.addEventListener("load", function() {
  console.log("image has loaded");
});

You'd probably want to do some error handling there too, as images can also emit an error event.

There is a good chance you're abstracting this concept, so you'd be watching for any number of images, in which you'd have to loop over them and all that. Plus images can be background-images too...

There is enough going on here that I'd suggest just using David DeSandro's imagesLoaded library. It has no dependencies, is pretty small, and works with jQuery if you're using that.

Here we're just watching for all images on the entire page to be loaded before testing widths:

imagesLoaded(document.body, function() {
  var img = document.querySelector("img");
  console.log(img.width);
  console.log(img.naturalWidth);
});

Why?

One use case for this I had recently was a lightbox-like thing. I wanted to make sure that if I was rendering an image smaller than it actually was, the user had the ability to click on it to open it up bigger.

// Only do this on large screens
if (window.outerWidth > 800) {

  // Wait for all images to load inside the article
  $("article").imagesLoaded(function() {

    $("figure > img").each(function(el) {

        // Only do this is shown image is smaller than actual image
        if (this.naturalWidth > this.width) {

          $(this)
            .closest("figure")
            .addClass("can-be-enlarged")
            .end()
            // When the image is clicked, toggle a class to enlarge it
            .on("click", function(e) {
              e.stopPropagation();
              $(this)
                .closest("figure")
                .toggleClass("enlarge");
            })
        }

      });

    });

    // When the enlarged image is clicked again, remove the class, shrinking it back down
    $("body").on("click", "figure.can-be-enlarged.enlarge", function() {
      $(this).removeClass("enlarge");
    })

  }
}

The enlarge class make the entire <figure> into a full-screen overlay with the image and figcaption centered within it. But it only did all this stuff if it made sense to do it, which needed the image with logic to be correct!