How to make a Javascript image comparison slider

Today, you’re going to learn to make a slider that can compare two images. This is good for doing things like before and after comparisons. The solution I will give you allows you to have thumbnail images and a slider to control it. I have used minimal styling to make it easier to just copy the entire thing into your existing website.

First of all, this post assumes that you know javascript to a decent degree, including the ES6+ changes. If you’re not familiar with JS, I will have some beginner posts up in the near future.

Below is a codepen for the finished product.

See the Pen BaKaabx by pr0uxx (@pr0uxx) on CodePen.

If you’re still here, it means you are genuinely interested in my thought process or you’re stuck trying to paste this into your website. I’ll assume the latter. It’s okay, here’s the breakdown:

HTML

The HTML in the code is made up of two elements. The first is a stylesheet link to bootstrap 4:

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" 
integrity="sha384-gOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

I’m basically using bootstrap 4 for the grid system and the slider. Everything will still work if you remove the stylesheet, it will just look all misshapen.

The second part of the HTML is just a div with a class. This is a wrapper for the code that we’re going to insert using the JS.

<div class="image-compare-list"></div>

Inside of the JS is where the bulk of the HTML lives. So the CSS section makes sense, it’s important that you understand the structure of the injected HTML:

<div class="row">
    <div class="col-1">
        <div id="selector"></div>
    </div>
    <div class="col-4">
        <div id="display">
            <div class="slide-container" id="slide-1">
            </div>
            <div class="slide-container" id="slide-2">
            </div>                   
        </div>
        <input id="slider" type="range" min="0" max="100" value="50">
    </div>
</div>

The first div is a bootstrap grid row. Everything in the col-* divs will adhere to this row:

<div class="row">...</div>

The second set of divs are columns for that row. The first column div will contain the thumbnails so has the smallest possible default clumn size. The second column div contains the slider and the comparison image:

<div class="col-1">
    first column div
</div>
<div class="col-4">
    second column div
</div>

Inside of the smaller column is the following div:

<div id="selector"></div>

The ‘selector‘ ID will later on be used in the JS to define where the image thumbnails will be placed. This code is generated dynamically according to the images you use… more on that later.

Inside of the second column div there is a div with a ‘display’ class and an input:

<div id="display">
    ...
</div>
<input id="slider" type="range" min="0" max="100" value="50">

The ‘display’ div is used as a container for the two images that you want to compare. The input is the slider itself, given attributes and an id to identify it later. If you want to change the position that your slider starts on, change the value attribute.

Finally, within the display div are two more divs, both with the class .slide-container and each with a unique ID. The class is used in CSS and the ID is referenced in the JS.

CSS

The custom CSS I’ve provided basically organises the overlaying of the two images. If you want to change the size of the comparison image, change the width value in the #display class and the height value in the .slide-container class. Keep both the height and width values the same or you may experience weird things.

So, class by class, let’s look at what’s happening:

#display {
    display: flex;
    width: 200px;
}

The display class is the outer most class for the comparison image. The width has to be defined here as it wraps the two images. I’m using display: flex to force the internal divs to be placed side by side

Next, the .slide-container class is responsible for holding each of the comparison images. It has a height property which I am using here to force both comparison images to have the same height. The background-repeat and background-size properties are then used in conjuction. background-repeat: no-repeat ensures that the background image of the div will never be repeated and background-size: cover makes the background the entire size of the div.

Third and fourth are the .slide-container:nth-of-type classes:

.slide-container:nth-of-type(1) {
    background-position: left;
}

.slide-container:nth-of-type(2) {
    background-position: right;
}

All this does is says, the first element with the .slide-container class will go on the left and the second element with the .slide-container class will go on the right. This is because both of those elements would otherwise occupy the same space within their container.

Finally, there are two remaining classes:

#slider {
    width: 200px;
}

.pb-image {
    width: 100%;
}

The #slider class defines the width of the slider, feel free to change it how you please. Funny story, I had comparing people’s faces in mind for this task so the .pb-image class actually stands for ‘person button image’. This sets the width of the thumbnail images .

Javascript

The Javascript is ordered almost backwards in terms of the execution. I’ve placed the executing code right at the bottom, which is possibly how you’ve managed to get near the bottom of this blog post.

Starting with the last line, we declare a new instance of the ImageCompareList class:

new ImageCompareList(people, 
document.getElementsByClassName('image-compare-list')[0]);

When we instantiate the class, we are passing through the the people array and the image compare list element. the second parameter is the container defined in our html with an index selector of 0 because the getElementsByClassName function returns an array as opposed to a single element.

Next, let’s take a look at the people object we’re passing to the ImageComparelist instance:

People Array

people = [
    new Person(0, 
        "https://via.placeholder.com/150/FFFF00/000000/?text=A-SIDE", 
        "https://via.placeholder.com/150/FF0000/000000/?text=A-SIDE", 
        "https://via.placeholder.com/150/FFFF00/000000/?text=A-SIDE"),
    new Person(1, 
        "https://via.placeholder.com/150/eeb39e/000000/?text=A-SIDE", 
        "https://via.placeholder.com/150/f8e8e2/000000/?text=A-SIDE", 
        "https://via.placeholder.com/150/eeb39e/000000/?text=A-SIDE")
    ]

Side note: I’ve taken out the last person object that’s in the actual code because the links are a bit longer and make the formatting on this website difficult to read.

The people array is literally an array of the Person class, so we need to look at the Person class to figure out what is really going on:

Person Class

class Person {
    constructor(id, image1, image2, image3) {
        this.id = id;
        this.image1 = image1;
        this.image2 = image2;
        this.image3 = image3;
    }
}

The Person class contains just lists four properties, each of which are set when the class is instantiated. That means, the values we pass through as parameters when making a new instance of the class will be set as the class properties.

Image Compare List

Now that we know about the Person class and the people object, it’s time to look at what happens when we instantiate the ImageCompareList class:

class ImageCompareList {
    constructor(people, element) {
        const baseHtml = `<div class="row">
            <div class="col-1">
                <div id="selector"></div>
            </div>
            <div class="col-4">
                <div id="display">
                    <div class="slide-container" id="slide-1">
                    </div>
                    <div class="slide-container" id="slide-2">
                    </div>                   
                </div>
               <input id="slider" type="range" min="0" max="100" value="50">
            </div>
        </div>`;
        element.innerHTML = baseHtml;
        this.peopleArray = people;
        this.buttonContainer = document.getElementById('selector');

        let buttonArr = [];

        const slider = new Slider(people[0]);

        new Promise(resolve => {
            for (let i = 0; i < this.peopleArray.length; i++) {
                const person = this.peopleArray[i];
                this.buttonContainer.innerHTML += `<div class='person-button' data-person-id='${person.id}' id='p-button${person.id}'><img class='pb-image' src='${person.image3}'></img></div>`;
                
            buttonArr.push(
                document.getElementById(`p-button${person.id}`));
            }
                resolve();
            }).then(() => {
                for (let i = 0; i < this.peopleArray.length; i++) {
                    document.getElementById(`p-button${i}`)
                        .addEventListener("click", (e) => {
                            slider.showPerson(this.peopleArray[i]);
                        });
                }
            });
        }
    }

This looks like a lot of code, but most of it is just html templating. First of all, there is the baseHtml constant. This is where I define the html that is going to contain the display you see when you view the page.

The next thing we’re going to do is actually insert that template html into the element that we passed in when instantiating the ImageCompareList class:

element.innerHTML = baseHtml;

The element variable passed in is a reference to the actual object in the HTML Document object model (DOM). That means, when the variable is updated, the DOM is also updated, allowing us to query the DOM in the next few lines of code and retrieve the code we just inserted.

The next line is going to assign our people parameter to a peopleArray property belonging to the ImageCompareList object:

this.peopleArray = people

The reason we do this is so that we still have access to the people array once we exit the scope of the class constructor. Later on, we will use the array in an anonymous function which is within the constructor physically, but logically in a different scope.

Next, we retrieve the div we defined in the baseHtml constant with the ‘selector’ id. This will allow us to insert more HTML into it a bit later on:

this.buttonContainer = document.getElementById('selector');

After that we simply assign an empty array to a variable for use later:

let buttonArr = [];

The next line of code is a little more important as it instantitates a new Slider object:

Slider class

const slider = new Slider(people[0]);

Here, I am instantiating the slider with the first item in the people array. This means that the first Person object you build into your array will be the first object to show on your slider. But what does the slider do? Let’s look at the code:

class Slider {
    constructor(person = null) {
        this.imageContainer1 = document.getElementById("slide-1");
        this.imageContainer2 = document.getElementById("slide-2");
        this.slider = document.getElementById("slider");

        this.imageContainer1.style.width = this.imageContainer2.style.width = "50%";
        if (person != null) this.showPerson(person);
        this.slider.addEventListener("input", (e) => { this.slide(e); })
    }
   
    ...
}

I’ve just included the constructor now so we can have a look at what happens during instantiation, we’ll take a look at the functions next. The first three lines of the constructore are dedicated to grabbing elements that we defined earlier in the DOM:

this.imageContainer1 = document.getElementById("slide-1");
this.imageContainer2 = document.getElementById("slide-2");
this.slider = document.getElementById("slider");

The next line might catch you off guard:

this.imageContainer1.style.width = this.imageContainer2.style.width = "50%";

All I’m doing here is setting the both containers to have a width of 50%. Chaining variable assignment is just syntactic sugar, the same is achieved by doing the following:

this.imageContainer1.style.width = "50%";
this.imageContainer2.style.width = "50%";

The next thing we’re going to do is a null check against the person parameter being provided. If the parameter is not null, we call the this.showPerson function:

if (person != null) this.showPerson(person);

When we examine the definition of this.showPerson we can see that it’s simply assigns the images we set in our people array to the DOM element background images:

showPerson(person) {
    this.imageContainer1.style.backgroundImage = `url('${person.image1}')`;
    this.imageContainer2.style.backgroundImage = `url('${person.image2}')`;
}

The last line of the constructor adds an event listener to the slider object, instructing the DOM slider to fire the slide function within the Slider class when an input event is received:

this.slider.addEventListener("input", (e) => { this.slide(e); })

Now, let’s take a look at the slide function

slide(event) {
    const a = event.target.value;
    const b = 100 - a;
    console.log(`sliding: ${a}:${b}`);
    this.setWidth(a, b);
}

This is a fairly simple function, it will take the slider value and offset it against 100. So, we have a slider in the DOM with a min value of 0 and a max value of 100. This allows us to use the slider to dictate how much of the total image space on of two images takes. So, the a and b variables are just the width percentage that each of the two comparison images should have.

Finally, we take the a and b variables and pass them into the following setWidth function:

setWidth(image1Width, image2Width) {
    this.imageContainer1.style.width = image1Width + '%';
    this.imageContainer2.style.width = image2Width + '%';
}

The last function, the simplest function. This simply takes our width variables and assigns them, as a percentage, to each image container.

Image Compare List Continued

Now that the slider class has finished instantiating, we can go back to where we were in the ImageCompareList class. The next line declares a new Promise which runs a loop, resolves then runs another loop.

new Promise(resolve => {
    for (let i = 0; i < this.peopleArray.length; i++) {
        const person = this.peopleArray[i];

        this.buttonContainer.innerHTML += `<div class='person-button' data-person-id='${person.id}' id='p-button${person.id}'><img class='pb-image' src='${person.image3}'></img></div>`;
        buttonArr.push(document.getElementById(`p-button${person.id}`));
    }

    resolve();
}).then(() => {
                for (let i = 0; i < this.peopleArray.length; i++) {
                    document.getElementById(
                    `p-button${i}`).addEventListener("click", (e) => {
                        slider.showPerson(this.peopleArray[i]);
                    });
                }
            });

First of all, I want to point out that this code will actually run fine without the promise, but if you were to go one step further and merge the loops, you would break the thumbnail buttons.

In the first loop we get the Person we want by id and append the Person‘s properites to our previously defined buttonContainer:

const person = this.peopleArray[i];

this.buttonContainer.innerHTML += `<div class='person-button' data-person-id='${person.id}' id='p-button${person.id}'><img class='pb-image' src='${person.image3}'></img></div>`;

What this does is adds to the string to the existing DOM. However, this doesn’t happen instantaneously. So, if we then retrieve the html we’ve just set, we’ll get nothing back, and the following code won’t work:

document.getElementById(`p-button${i}`).addEventListener("click", (e) => {
    slider.showPerson(this.peopleArray[i]);
})

When we split this into two seperate loops, enough time is elapsed during execution that the DOM will be updated before we attempt to retrieve the element again. The promise is then introduced to explicitly inform any future developers that the first loop must happen after the second loop, at least making them question why there is a promise before they introduce a bug we’ve already averted.

The very last line of code that we’re going to look at is the event listener and that we’re adding in our second loop:

document.getElementById(`p-button${i}`).addEventListener("click", (e) => {
    slider.showPerson(this.peopleArray[i]);
})

This adds a showPerson function with our selected person to the button that we’re selecting. As we’ve previously discussed, the show person function will take the images set in our current person and apply them as the images in the main slider image.

That’s it. That’s how the whole thing works. The most important bit is being aware that the slide function gets called any time the user moves the slider. The code inside of the event listener gets called and the images and their width get reset.

Leave a Reply

Your email address will not be published. Required fields are marked *