Image Zoom with plain JavaScript and CSS

20 March 2017

The premise is simple. A post may contain images. These images are restricted in rendered size to keep the flow of the page in tact. Clicking an image allows you to zoom in. Here’s an example:

Go ahead, click that bunny!

Sample zoomable image

The CSS

Let’s get the CSS out of the way first. The selector used is article img, which means any image in the post. By default I limit it to a maximum width of its parent container. Also, I change the cursor to a pointer, to indicate you can click on the image, like a link.

article img {
    max-width: 100%;
    cursor: pointer;
}

Then there are images with the zoomed class. This is still the same image element, but with an additional class:

article img.zoomed {
    position: fixed;

    top: 5vh;
    bottom: 5vh;
    left: 5vw;
    right: 5vw;

    max-width: 90vw;
    max-height: 90vh;

    margin: auto;

    border: 4px solid #000
}

Okay, that’s a bit more CSS, but this basically overlays the image on to the page and adds some whitespace around it.

The trick to zooming is adding the zoomed class to the img element. Zooming out means removing that zoomed class again.

Now, on to the JavaScript…

The jQuery solution

For years now jQuery has been my go-to tool for anything JavaScript, mainly because it comes bundled with Rails. (Yes, I used prototype as well in the old days.)

The jQuery solution is as you rather straight forward. Wait for the DOM to be loaded, and handle click events on all article img elements. When clicked, toggle the zoomed class.

$(function() {
    $(document).on('click', 'article img', function() {
        $(this).toggleClass('zoomed');
    });
}

Because I’m a keyboard user (Vim, not Emacs, thank you), I prefer to map ESC to also close any zoomed imaged.

$(function() {
    $(document).keyup(function(e) {
        if (e.keyCode == 27) {
            $('img.zoomed').each(function(idx) {
                $(this).toggleClass('zoomed');
            });
        }
    }
}

Again, hook into the keyup event, check if ESC was pressed and toggle the zoomed class for all zoomed in images.

But, using jQuery means adding an extra dependency: 1 extra HTTP request and 85kB download for you. Also, the few friends I have who practice JavaScript tell me that pure JavaScript is the way to go these days. So, let’s try!

The JavaScript solution

With some help from the You Might Not Need jQuery website, I managed to drop the 85kB big jQuery dependency and rewrite the above functionality in plain old JavaScript.

First, let’s write a function that waits for the DOM to load.

function ready(fn) {
    if (document.readyState != 'loading') {
        fn();
    } else {
        document.addEventListener('DOMContentLoaded', fn);
    }
}

This was taken straight from You Might Not Need jQuery to wrap any functions you want to run when the document has loaded fully.

Next I wrote a function to handle toggling the zoomed CSS class on the images:

function imageClick(e) {
    e.preventDefault();
    this.classList.toggle('zoomed');
}

It turns out that JavaScript is more than capable of toggling classes on its own.

While we’re at it, let’s also write the function that handles the ESC presses.

function handleEsc(e) {
    if (e.keyCode == 27) {
        var zoomedImages = document.querySelectorAll('img.zoomed');
        Array.prototype.forEach.call(zoomedImages, function(el, i) {
            el.classList.toggle('zoomed');
        });
    }
}

This is a bit more involed. I still check for the proper keyCode, and then proceed to find all zoomed images using document.querySelectorAll. It’s really that easy.

Next I use the Array prototype to map a function to each zoomed image. That function simply toggles the zoomed class, just like imageClick does.

What remains is nothing more than some glue to put the above fuctions together.

ready(function() {
    var images = document.querySelectorAll('article img');
    Array.prototype.forEach.call(images, function(el, i) {
        el.addEventListener('click', imageClick);
    });

    document.addEventListener('keyup', handleEsc);
});

Here I use the ready function I wrote. Just like handleEsc, I find all article img elements and add the event listener for clicks. Then I also add an event listener for the ESC key.

Conclusion

Rewriting a trivial piece of jQuery code to plain JavaScript appears to be more than worth the while. Besides the warm fuzzy feeling of dumping jQuery, it saves quite a few kilobytes from each page on devroom.io. Especially for mobile users this matters.

git says 11 deletions (bye, jQuery) and 27 additions (hello, JavaScript). This does not tell the full story, as one of these deleted lines is this one:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

I can highly recommend you take a closer look at what your jQuery code is actually doing and consider moving away from unnecessary dependencies. Yay for lean and mean web pages!