Image Comparison Slider

An interactive before-and-after image comparison slider that lets users visually explore differences with drag control.

Before imageAfter image

1. Copy the component file

Create a new file called image-comparison-slider.jsx in your reusable components folder (for example /src/components/) and paste the following code into it.

image-comparison-slider.jsx

import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import styles from "./image-comparison-slider.module.css";

const ImageComparisonSlider = (props) => {
  const {
    beforeImage,
    afterImage,

    imageWidth,
    imageHeight,

    defaultPercentage = 50,

    // controlled props
    percentage,
    onSliderChange,

    sliderStyles = {},
  } = props;

  const [sliderPercentage, setSliderPercentage] = useState(defaultPercentage);
  const [imageDimensions, setImageDimensions] = useState({
    width: 0,
    height: 0,
  });

  const imageSliderWrapperNode = useRef(null);

  useEffect(() => {
    const resizeObserver = new ResizeObserver((entries) => {
      const imageSliderWrapperDomNode = entries[0];
      const { width, height } = imageSliderWrapperDomNode.contentRect;
      setImageDimensions({
        width,
        height,
      });
    });
    resizeObserver.observe(imageSliderWrapperNode.current);
    return () => {
      resizeObserver.disconnect();
    };
  }, [beforeImage?.src, afterImage?.src]);

  const beforeImageClipWidth = useMemo(() => {
    const { width } = imageDimensions;
    return (width * (100 - sliderPercentage) / 100) || 0;
  }, [imageDimensions, sliderPercentage]);

  useLayoutEffect(() => {
    Object.entries({
      "--before-image-clip-width": `${beforeImageClipWidth || 0}px`,
      "--slider-width": `${parseInt(sliderStyles?.size) || 8}px`,
      "--slider-height": `${imageDimensions?.height || 0}px`,
      "--slider-border-color": sliderStyles?.borderColor || "#dddddd",
      "--slider-background-color": sliderStyles?.backgroundColor || "#5b83dc",
      "--slider-active-border-color": sliderStyles?.activeBorderColor || "#e25a5a",
      "--slider-active-background-color": sliderStyles?.activeBackgroundColor || "#e25a5a",
    }).forEach(([key, value]) => {
      imageSliderWrapperNode.current.style.setProperty(key, value);
    });
  }, [
    beforeImageClipWidth,
    sliderStyles?.size,
    sliderStyles?.borderColor,
    sliderStyles?.backgroundColor,
    sliderStyles?.activeBackgroundColor,
    sliderStyles?.activeBorderColor,
    imageDimensions?.height
  ]);

  if (
    percentage !== undefined &&
    percentage !== sliderPercentage
  ) {
    setSliderPercentage(percentage);
  }

  const handleChange = (e) => {
    const sliderValue = +e.target.value;
    setSliderPercentage(sliderValue);
    onSliderChange?.(sliderValue);
  };

  return (
    <div
      className={styles["image-comparison-slider"]}
      ref={imageSliderWrapperNode}
    >
      <img
        className={styles["before-image"]}
        src={beforeImage.src}
        alt={beforeImage.alt || ""}
      />
      <img
        className={styles["after-image"]}
        src={afterImage.src}
        alt={afterImage.alt || ""}
        style={{
          width: imageWidth,
          height: imageHeight,
        }}
      />
      <input
        type="range"
        min="0"
        max="100"
        step="1"
        value={sliderPercentage}
        onChange={handleChange}
      />
    </div>
  );
};

export default memo(ImageComparisonSlider, (prevProps, nextProps) => {
  return (
    [
      "imageWidth",
      "imageHeight",
      "percentage",
      "onSliderChange",
    ].every(key => (
      prevProps?.[key] === nextProps?.[key]
    )) &&
    [
      "src",
      "alt"
    ].every(key => (
      prevProps?.beforeImage?.[key] === nextProps?.beforeImage?.[key] &&
      prevProps?.afterImage?.[key] === nextProps?.afterImage?.[key]
    )) &&
    [
      "size",
      "borderColor",
      "backgroundColor",
      "activeBackgroundColor",
      "activeBorderColor",
    ].every(key => (
      prevProps?.sliderStyles?.[key] === nextProps?.sliderStyles?.[key]
    ))
  );
});

2. Copy the CSS module file

In the same folder, create a file called image-comparison-slider.module.css and paste the following CSS.

image-comparison-slider.module.css

.image-comparison-slider {
  --before-image-clip-width: 0;
  --slider-width: 8px;
  --slider-height: 0;
  --slider-background-color: #5b83dc;
  --slider-border-color: #dddddd;
  --slider-active-background-color: #e25a5a;
  --slider-active-border-color: #e25a5a;

  box-sizing: border-box;
  display: inline-block;
  position: relative;

  img,
  input {
    all: unset;
    user-select: none;
  }

  .before-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    clip-path: inset(0 var(--before-image-clip-width, 0) 0 0);
  }

  .after-image {
    vertical-align: middle;
  }

  input[type="range"] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    position: absolute;
    top: 0px;
    left: 0px;
    z-index: 2;
    width: 100%;
    height: 100%;
    margin: 0;
    background: transparent;
    overflow: hidden;

    &::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      box-sizing: border-box;
      display: block;
      border: 1px solid;
      border-color: var(--slider-border-color, "#dddddd");
      background: var(--slider-background-color, "#5b83dc");
      border-radius: 0;
      width: var(--slider-width, 8px);
      height: var(--slider-height, 0);
      cursor: pointer;
    }

    &::-moz-range-thumb {
      -moz-appearance: none;
      appearance: none;
      box-sizing: border-box;
      display: block;
      border: 1px solid;
      border-color: var(--slider-border-color, "#dddddd");
      background: var(--slider-background-color, "#5b83dc");
      border-radius: 0;
      width: var(--slider-width, 8px);
      height: var(--slider-height, 0);
      cursor: pointer;
    }

    &::-webkit-slider-thumb:active {
      background: var(--slider-active-background-color, "#e25a5a");
      border-color: var(--slider-active-border-color, "#e25a5a");
      transition: all 250ms ease-in-out;
    }

    &::-moz-range-thumb:active {
      background: var(--slider-active-background-color, "#e25a5a");
      border-color: var(--slider-active-border-color, "#e25a5a");
      transition: all 250ms ease-in-out;
    }
  }
}

3. Use the component

Now you can import and use the component anywhere in your project.

image-comparison-slider-preview.jsx

import ImageComparisonSlider from "@/components/essentials/image-comparison-slider/image-comparison-slider";

const ImageComparisonSliderPreview = () => {
  return (
    <ImageComparisonSlider
      beforeImage={{
        src: "https://picsum.photos/id/65/800/450?grayscale",
        alt: "Before image"
      }}
      afterImage={{
        src: "https://picsum.photos/id/65/800/450",
        alt: "After image"
      }}
      imageWidth="480px"
    />
  )
};

export default ImageComparisonSliderPreview;

Default Slider Value

Before imageAfter image

image-comparison-slider

import ImageComparisonSlider from "@/components/essentials/image-comparison-slider/image-comparison-slider";

const ImageComparisonSliderDefaultPercentagePreview = () => {
  return (
    <ImageComparisonSlider
      beforeImage={{
        src: "https://picsum.photos/id/65/800/450?grayscale",
        alt: "Before image"
      }}
      afterImage={{
        src: "https://picsum.photos/id/65/800/450",
        alt: "After image"
      }}
      defaultPercentage={20}
      imageWidth={320}
    />
  )
};

export default ImageComparisonSliderDefaultPercentagePreview;

Customize Slider Styles

Before imageAfter image

image-comparison-slider

import ImageComparisonSlider from "@/components/essentials/image-comparison-slider/image-comparison-slider";

const ImageComparisonSliderStyleSliderPreview = () => {
  return (
    <ImageComparisonSlider
      beforeImage={{
        src: "https://picsum.photos/id/65/800/450?grayscale",
        alt: "Before image"
      }}
      afterImage={{
        src: "https://picsum.photos/id/65/800/450",
        alt: "After image"
      }}
      imageWidth={320}
      sliderStyles={{ 
        size: 16,
        borderColor: "black",
        backgroundColor: "white", 
        activeBorderColor: "black",
        activeBackgroundColor: "black" 
      }}
    />
  )
};

export default ImageComparisonSliderStyleSliderPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
beforeImage{ src: string; alt: string }YesObject containing src and alt for the before image.
afterImage{ src: string; alt: string }YesObject containing src and alt for the after image.
imageWidthstring | numberNoundefinedSets the width of the image (px or %).
imageHeightstring | numberNoundefinedSets the height of the image (px or %).
defaultPercentagenumberNo50Initial slider position percentage (used in uncontrolled mode).
percentagenumberNoundefinedControlled value to manually set the slider position.
onSliderChange(percentage: number) => voidNoundefinedCallback triggered when the slider position changes.
sliderStyles{ size?: string | number; borderColor?: string; backgroundColor?: string; activeBorderColor?: string; activeBackgroundColor?: string }NoundefinedCustomizes slider appearance including size, border and background colors (default and active states).