Image comparison slider in react

Learn how to create image comparison slider in react.

Image comparison slider will compare two images by sliding one image over the other with help of a slider.

image comparison slider

Now as you have got a good idea about what are going to build. Let us start the development.

There are few extra packages which we will require.

  • prop-types: To validate the props.
  • classnames: With the help of this we can use CSS classes as javascript objects, we just have to name our CSS file as filename.module.css.

Begin creating the component by importing all the required packages at the top.

import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "./index.module.css";
import cx from "classnames";

class ImageComparisonSlider extends Component {
  //Other code will go here
}

export default ImageComparisonSlider;

We have created a stateful component using class because we need to maintain certain state which you will find out later.

There are few things that are essential to create this type of components.

  • Two images which will be compared with one another.
  • Dimension of the comparison area.

Validate the props initially so that we can set the default props in case required props are missing.

  static propTypes = {
    image1: PropTypes.string.isRequired,
    image2: PropTypes.string.isRequired,
    width: PropTypes.string.isRequired,
    height: PropTypes.string.isRequired
  };

  static defaultProps = {
    width: "500px",
    height: "500px"
  };

Comparison area dimensions can be omitted that is why we have set the defaults for it, but we need the images.

Let us first create the layout of the component so that we can get a better understanding of which and where events should be placed.

render() {
    const { image1, image2, width, height } = this.props;

    const events = {
      onMouseDown: this.slideStart,
      onTouchStart: this.slideStart
    };

    const dimension = {
      width,
      height
    };

    return (
      <div className={styles.container} style={dimension}>
        <div className={styles.image}>
          <img src={image1} style={dimension} alt="Compare 1" />
        </div>
        <div
          className={cx(styles.image, styles.overlay)}
          ref={e => (this.overlayRef = e)}
        >
          <img
            src={image2}
            ref={e => (this.imageRef = e)}
            style={dimension}
            alt="Compare 2"
          />
        </div>
        <span
          className={styles.slider}
          {...events}
          ref={e => (this.sliderRef = e)}
        ></span>
      </div>
    );
  }

We have created a relative container and placed the two images inside two div and also create a slider element which will be used as a handle for sliding the images.

All the elements inside the container will absolute positioned and the order of placement according to z-index will be like this.

  • Second image parent div at the bottom.
  • First image parent div above the second image.
  • Slider handle above the first image container div.

We have created refs for the three different elements because this way we can fetch and update the elements directly without updating the state.

This results in much better performance.

Also if you see we have added two events onMouseDown and onTouchStart on the slide handler because the image sliding will start with the help of these.

A slide event is composed of three different events.

  1. onMouseDown:- Which represents you have pressed the mouse button.
  2. onMouseMove:- Which represents you are moving the mouse around.
  3. onMouseUp:- Means you have released the mouse button.

The same goes for touch.

  1. onTouchStart
  2. onTouchMove
  3. onTouchEnd

So lets create the state and different functions for slide.

state = {
    canStart: false,
    imageWidth: 0
  };

  componentDidMount() {
    if (this.imageRef) {
      this.setState({
        imageWidth: this.imageRef.offsetWidth
      });
    }
  }

There are two things we are maintaining in the state.

1. imageWidth:- we are updating this when the component has fully rendered inside the componentDidMount() lifecycle method.

You will be thinking that I could have used the width that I had received in the props over here. Well there two problems with that.

First, the width comes appended with its types like 300px which I cannot use, we just need the number.

Second, width can be passed with other metrics also like 40vw, 70%, etc, but I want the value in pixels only because we are going to need it for calculation.

2. canStart: The moment user has pressed the mouse button on the slide handler we will update the state that user is now ready for sliding.

The major reason to maintain this is that mouse button is pressed on the handler but the mouse will move on the whole container area.

We cannot listen to the onMouseEvent on the handler instead we will have to listen it on the whole window, so while sliding we will have to make sure that the slider handler is pressed and then only mouse is moving.

This will update the state when user presses or releases the mouse button.

  slideStart = e => {
    e.preventDefault();
    this.setState({
      canStart: true
    });
  };

  slideEnd = e => {
    e.preventDefault();
    this.setState({
      canStart: false
    });
  };

Create the function for image comparison slider.

  componentDidMount() {
    //Image sliding
    window.addEventListener("mousemove", this.slideMove);
    window.addEventListener("touchmove", this.slideMove);

    //Sliding stopped
    window.addEventListener("mouseup", this.slideEnd);
    window.addEventListener("touchend", this.slideEnd);

    if (this.imageRef) {
      this.setState({
        imageWidth: this.imageRef.offsetWidth
      });
    }
  }

  slideMove = e => {
    let pos,
      w = this.state.imageWidth;
    const { canStart } = this.state;

    //If not ready then return false
    if (!canStart) return false;

    //If image and slider ref not ready then return false
    if (!this.sliderRef || !this.overlayRef) return false;

    /* Get the cursor's x position: */
    pos = this.getCursorPos(e);

    /* Prevent the slider from being positioned outside the image: */
    if (pos < 0) pos = 0;
    if (pos > w) pos = w;

    /* Execute a function that will resize the overlay image according to the cursor: */
    this.slide(pos);
  };

  getCursorPos = e => {
    let a,
      x = 0;
    e = e || window.event;

    /* Get the x positions of the image: */
    a = this.imageRef.getBoundingClientRect();

    /* Calculate the cursor's x coordinate, relative to the image: */
    x = e.pageX - a.left;

    /* Consider any page scrolling: */
    x = x - window.pageXOffset;

    return x;
  };

  slide = x => {
    /* Resize the image: */
    this.overlayRef.style.width = x + "px";

    /* Position the slider: */
    this.sliderRef.style.left = x + "px";
  };

The slideMove function will validate everything like can we start the slide? and all the refs are proper are not because we will need them for updating themselves.

Then get the current cursor position of the mouse using the getCursorPos and check if it is outside the container area or not. If it is then limit it to the container area, so that no alien things happen.

In the end update the width of the overlay or the second image parent div and the position of the slider handler.

Updating the parent div width and keeping the content inside it hidden will only show the image in the visible width instead of resizing the image itself.

The last thing is to remove all the event listeners when component will be removed.

  componentWillUnmount() {
    window.removeEventListener("mouseup", () => {});
    window.removeEventListener("touchend", () => {});
    window.removeEventListener("mousemove", () => {});
    window.removeEventListener("touchmove", () => {});
  }

Complete code of image comparison slider in react.

import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "./index.module.css";
import cx from "classnames";

class ImageComparisonSlider extends Component {
  static propTypes = {
    image1: PropTypes.string.isRequired,
    image2: PropTypes.string.isRequired,
    width: PropTypes.string.isRequired,
    height: PropTypes.string.isRequired
  };

  static defaultProps = {
    width: "500px",
    height: "500px"
  };

  state = {
    canStart: false,
    imageWidth: 0
  };

  slideStart = e => {
    e.preventDefault();
    this.setState({
      canStart: true
    });
  };

  slideEnd = e => {
    e.preventDefault();
    this.setState({
      canStart: false
    });
  };

  componentDidMount() {
    //Image sliding
    window.addEventListener("mousemove", this.slideMove);
    window.addEventListener("touchmove", this.slideMove);

    //Sliding stopped
    window.addEventListener("mouseup", this.slideEnd);
    window.addEventListener("touchend", this.slideEnd);

    if (this.imageRef) {
      this.setState({
        imageWidth: this.imageRef.offsetWidth
      });
    }
  }

  slideMove = e => {
    let pos,
      w = this.state.imageWidth;
    const { canStart } = this.state;

    //If not ready then return false
    if (!canStart) return false;

    //If image and slider ref not ready then return false
    if (!this.sliderRef || !this.overlayRef) return false;

    /* Get the cursor's x position: */
    pos = this.getCursorPos(e);

    /* Prevent the slider from being positioned outside the image: */
    if (pos < 0) pos = 0;
    if (pos > w) pos = w;

    /* Execute a function that will resize the overlay image according to the cursor: */
    this.slide(pos);
  };

  getCursorPos = e => {
    let a,
      x = 0;
    e = e || window.event;

    /* Get the x positions of the image: */
    a = this.imageRef.getBoundingClientRect();

    /* Calculate the cursor's x coordinate, relative to the image: */
    x = e.pageX - a.left;

    /* Consider any page scrolling: */
    x = x - window.pageXOffset;

    return x;
  };

  slide = x => {
    /* Resize the image: */
    this.overlayRef.style.width = x + "px";
    /* Position the slider: */
    this.sliderRef.style.left = x + "px";
  };

  componentWillUnmount() {
    window.removeEventListener("mouseup", () => {});
    window.removeEventListener("touchend", () => {});
    window.removeEventListener("mousemove", () => {});
    window.removeEventListener("touchmove", () => {});
  }

  render() {
    const { image1, image2, width, height } = this.props;

    const events = {
      onMouseDown: this.slideStart,
      onTouchStart: this.slideStart
    };

    const dimension = {
      width,
      height
    };

    return (
      <div className={styles.container} style={dimension}>
        <div className={styles.image}>
          <img src={image1} style={dimension} alt="Compare 1" />
        </div>
        <div
          className={cx(styles.image, styles.overlay)}
          ref={e => (this.overlayRef = e)}
        >
          <img
            src={image2}
            ref={e => (this.imageRef = e)}
            style={dimension}
            alt="Compare 2"
          />
        </div>
        <span
          className={styles.slider}
          {...events}
          ref={e => (this.sliderRef = e)}
        ></span>
      </div>
    );
  }
}

export default ImageComparisonSlider;

Complete style code of image comparison slider

.container {
  position: relative;
  display: inline-block;
  box-shadow: 0 1px 3px #000;
}

.image {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.image > img {
  display: inline-block;
  vertical-align: middle;
}

.overlay {
  width: 50%;
  z-index: 1;
}

.slider {
  position: absolute;
  width: 50px;
  height: 50px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  background-color: red;
  border-radius: 50%;
  cursor: ew-resize;
}

Input

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import ImageComparisonSlider from "./Components/ImageComparisonSlider";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <div className="abc">
    <ImageComparisonSlider
      image1={
        "https://cdn.pixabay.com/photo/2019/12/30/13/10/lost-places-4729640_1280.jpg"
      }
      image2={
        "https://cdn.pixabay.com/photo/2018/09/16/15/31/boy-3681679_1280.jpg"
      }
    />
  </div>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();