Image Comparison Slider
An interactive before-and-after image comparison slider that lets users visually explore differences with drag control.
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
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
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:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| beforeImage | { src: string; alt: string } | Yes | — | Object containing src and alt for the before image. |
| afterImage | { src: string; alt: string } | Yes | — | Object containing src and alt for the after image. |
| imageWidth | string | number | No | undefined | Sets the width of the image (px or %). |
| imageHeight | string | number | No | undefined | Sets the height of the image (px or %). |
| defaultPercentage | number | No | 50 | Initial slider position percentage (used in uncontrolled mode). |
| percentage | number | No | undefined | Controlled value to manually set the slider position. |
| onSliderChange | (percentage: number) => void | No | undefined | Callback triggered when the slider position changes. |
| sliderStyles | { size?: string | number; borderColor?: string; backgroundColor?: string; activeBorderColor?: string; activeBackgroundColor?: string } | No | undefined | Customizes slider appearance including size, border and background colors (default and active states). |