How to build a responsive and lightweight image comparison slider

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>

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 {
    z-index: 10;
    &:before {
      content: "";
      position: absolute;
      top: 0;
      right: 0;
      width: 5px;
      height: 100%;
      border-right: 5px solid #FFF;
      z-index: 20;
    }
  }
  .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;
}

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>

This script basically looks for all image-slider-wrapper elements on the page and runs the imageSlider function on all instances, meaning you can have multiple image sliders on one page at any one time.

This script also takes care of the image resizing for you, meaning if the browser is resized in any way, the images will update along with it.

This is an important part of the solution because it isn’t enough to rely on object-fit:cover alone, due to the fact that the image will be resizing relative to it’s container as the handler is changed. This is why we need to set an explicit width on both of the images for this method to work properly.

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. :)