How to build a responsive and lightweight image comparison slider using JavaScript

Last modified March 21, 2021
.* :☆゚

Creating image comparison sliders can be very finicky, and it is surprisingly really hard to find a good example out in the wild that is minimal, compact, and easily extensible.

There are also many pitfalls when creating an image comparison slider, some of which you may have encountered if you’ve already tried using third-party plugins and scripts from the first page of google.

The main issues I encountered were iffy useability on touchscreen devices as well as image alignment and sizing issues in relation to the slider and handle.

Here is my own solution to the problem, which utilises the input range field for it’s base functionality.


Below is a working example of what we’re building today:

Nature
Tech
Move the slider to toggle a comparison between two images.

Recreating this effect is made quite simple by using the range slider. The advantage of using a range slider as it’s base is that it builds upon in-browser technology, meaning this method is cross-browser compatible and easily accessible and useable on touch screen devices, unlike other solutions which involve scroll and gesture-jacking through JavaScript.

The markup

Paste the following markup into your code where you want the image comparison slider to appear.

<div class="image-slider-wrapper">

  <div class="image-slider">
    <div class="top-image">
      <img src="https://source.unsplash.com/800x400?nature" alt="Nature" class="comparison-img after">
    </div>
    <img src="https://source.unsplash.com/800x400?tech" alt="Tech" class="comparison-img before">

    <div class="handle"></div>

    <input type="range" class="range-slider" min="0" max="100" value="50"/>
  </div>

  <div class="caption">
    Move the slider to toggle a comparison between two images.
  </div>
</div>

This method requires a few wrapping divs so that the two images and the range slider can be overlaid on top of each other seamlessly.

I’ve also created a custom .handle element to act as the control for the slider. Creating a custom handle will not only override the default range slider UI, but also give us more control over the functionality and ensure the handle can reach end-to-end, unlike many other comparison sliders out there.

The CSS

Paste the following CSS in your styles. Note this is in SCSS syntax.

.image-slider-wrapper {
  max-width: 100%;
  overflow: hidden;
  margin: 30px auto;
  .caption {
    padding: 20px;
    text-align: center;
    font-size:14px;
    opacity: .7;
  }
}
.image-slider {
  position: relative;
  display: block;
  width: 100%;
  min-height: 600px;
  height: 50vh;
  display: flex;
  align-content: flex-end;
  > div {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 50%;
    overflow: hidden;
    display: flex;
    align-items: flex-end;
  }
  .comparison-img {
    display: block;
    user-select: none;
    box-sizing: border-box;
    height: 100%;
    width: 100%;
    max-width: initial;
    filter: grayscale(100%);
    opacity: 0.5;
    pointer-events: none;
    object-fit: cover;
    flex: none;
  }
  .top-image {
    position: relative;
    z-index: 10;
    &:before {
      content: "";
      position: absolute;
      top: 0;
      right: 0;
      width: 5px;
      height: 100%;
      border-right: 5px solid #FFF;
      z-index: 20;
    }
  }

  //this will act as the controller for the slider
  .handle {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 20px;
    background-color: #666666;
    height: 80px;
    border: 8px solid #FFF;
    z-index: 30;
    pointer-events: none;
    margin-left: -2px;
    overflow: visible;
    &:before {
      color: #FFF;
      content: "";
      position: absolute;
      top: 25px;
      right: -20px;
      height: 10px;
      width: 10px;
      border-bottom: solid 3px currentColor;
      border-right: solid 3px currentColor;
      transform: rotate(-45deg);
      z-index: 99;
    }
    &:after {
      color: #FFF;
      content: "";
      position: absolute;
      top: 25px;
      left: -20px;
      height: 10px;
      width: 10px;
      border-left: solid 3px currentColor;
      border-top: solid 3px currentColor;
      transform: rotate(-45deg);
      z-index: 99;
    }
  }
  input[type=range] {
    margin: 0;
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0px;
    height: 100%;
    width: 100%;
    background: transparent;
    padding: 0;
    border: none;
    z-index: 20;
    -webkit-appearance: none;
    appearance: none;
    &:focus {
      outline: none;
    }
  }
}

.image-slider input
.image-slider input[type=range],
.image-slider input[type=range]::-webkit-slider-runnable-track,
.image-slider input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
}
.image-slider input[type=range]::-webkit-slider-thumb {
  width: 20px;
  height: 500px;
  background-color: transparent;
  cursor: ew-resize;
  display: block;
  border: none;
}

Thiese styles essentially position each image and the range slider over the top of each other using position:absolute. By resetting the range slider’s appearance, I’ve also created a custom control using the .handle element.

You can of course tinker with these styles later so that the aesthetic matches the design of the website you’re working on.

Notice how the range slider is also spanning the entire wrapping div? Even though we hid the UI using appearance:none, the slider is still able to gauge input and act accordingly to mouse clicks and taps on mobile devices.

This is why I love using this technique so much; when you click anywhere on the element, the slider will immediately react to that event. This is exceedingly hard to achive using pure JavaScript alone, and even more so when we get into touch screen territory!

The JS

Paste the following script into your file and ensure it is being executed when the page loads.

<script type="text/javascript">
  function imageSlider(elem) {
    var imageContainer = elem;
    var overlay = imageContainer.querySelector(".top-image");
    var range = imageContainer.querySelector(".range-slider");
    var images = imageContainer.querySelectorAll(".comparison-img");
    var handle = imageContainer.querySelector(".handle");
    images.forEach(function(elem) {
      elem.style.width = range.offsetWidth + 'px'
    })
    range.oninput = function() {
      if ( this.value > 0 ) {
        overlay.style.width = this.value + "%";
        handle.style.left = this.value + "%";
      } else {
        overlay.style.width = 'calc(0% + 5px)';
        handle.style.left = 'calc(0% + 5px)';
      }
    }
  }

  const sliders = document.querySelectorAll('.image-slider-wrapper');
  function initImageSliders() {
    console.log('test')
    for (var i = 0; i < sliders.length; i++) {
      imageSlider(sliders[i]);
    }
  }

  initImageSliders();
  window.onresize = initImageSliders;
  window.dispatchEvent(new Event('resize'));

</script>

The script essentially calculates the block’s width in pixels, and sets that width on both images explicitly. This is fundamental to this method because it isn’t enough to rely on object-fit:cover alone, due to the fact that the image will resize relative to it’s container. By setting a width on each image, we can ensure that as the top image’s wrapper changes width, the image will remain it’s own original size.

This script will also look for all image-slider-wrapper elements on the page and run the imageSlider() function on all instances, meaning you can have multiple image sliders on one page at any one time.

I’ve also taken the liberty of attaching a resize event to this function, meaning that if the browser is resized in any way, the images will update along with it (try it out on the example above).

And that’s it!

I hope this method helped you achieve the perfect, simple image-comparison slider, and with minimal effort too. You are now free to change the image slider’s style and look according to your design. Let me know how you go if you use this method!