Image Comparison
Interactively compare two images with a draggable slider to reveal differences.


import {
ImageComparison,
ImageComparisonImage,
ImageComparisonSlider
} from "@/components/core/image-comparison";
export default function ImageComparisonBasic() {
return (
<ImageComparison className="aspect-16/9 w-full rounded-lg" enableHover>
<ImageComparisonImage
src="https://bundui-images.netlify.app/blog/01.jpg"
className="grayscale"
alt="Motion Primitives Dark"
position="left"
/>
<ImageComparisonImage
src="https://bundui-images.netlify.app/blog/01.jpg"
alt="Motion Primitives Light"
position="right"
/>
<ImageComparisonSlider className="w-0.5 bg-white/30 backdrop-blur-xs">
<div className="absolute top-1/2 left-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white"></div>
</ImageComparisonSlider>
</ImageComparison>
);
}
Installation
Install the following dependencies:
npm install clsx tailwind-merge motion
Copy and paste the following code into your project:
"use client";
import { cn } from "@/lib/utils";
import { useState, createContext, useContext } from "react";
import {
motion,
MotionValue,
SpringOptions,
useMotionValue,
useSpring,
useTransform
} from "motion/react";
const ImageComparisonContext = createContext<
| {
sliderPosition: number;
setSliderPosition: (pos: number) => void;
motionSliderPosition: MotionValue<number>;
}
| undefined
>(undefined);
export type ImageComparisonProps = {
children: React.ReactNode;
className?: string;
enableHover?: boolean;
springOptions?: SpringOptions;
};
const DEFAULT_SPRING_OPTIONS = {
bounce: 0,
duration: 0
};
function ImageComparison({
children,
className,
enableHover,
springOptions
}: ImageComparisonProps) {
const [isDragging, setIsDragging] = useState(false);
const motionValue = useMotionValue(50);
const motionSliderPosition = useSpring(motionValue, springOptions ?? DEFAULT_SPRING_OPTIONS);
const [sliderPosition, setSliderPosition] = useState(50);
const handleDrag = (event: React.MouseEvent | React.TouchEvent) => {
if (!isDragging && !enableHover) return;
const containerRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const x =
"touches" in event
? event.touches[0].clientX - containerRect.left
: (event as React.MouseEvent).clientX - containerRect.left;
const percentage = Math.min(Math.max((x / containerRect.width) * 100, 0), 100);
motionValue.set(percentage);
setSliderPosition(percentage);
};
return (
<ImageComparisonContext.Provider
value={{ sliderPosition, setSliderPosition, motionSliderPosition }}>
<div
className={cn(
"relative overflow-hidden select-none",
enableHover && "cursor-ew-resize",
className
)}
onMouseMove={handleDrag}
onMouseDown={() => !enableHover && setIsDragging(true)}
onMouseUp={() => !enableHover && setIsDragging(false)}
onMouseLeave={() => !enableHover && setIsDragging(false)}
onTouchMove={handleDrag}
onTouchStart={() => !enableHover && setIsDragging(true)}
onTouchEnd={() => !enableHover && setIsDragging(false)}>
{children}
</div>
</ImageComparisonContext.Provider>
);
}
const ImageComparisonImage = ({
className,
alt,
src,
position
}: {
className?: string;
alt: string;
src: string;
position: "left" | "right";
}) => {
const { motionSliderPosition } = useContext(ImageComparisonContext)!;
const leftClipPath = useTransform(motionSliderPosition, (value) => `inset(0 0 0 ${value}%)`);
const rightClipPath = useTransform(
motionSliderPosition,
(value) => `inset(0 ${100 - value}% 0 0)`
);
return (
<motion.img
src={src}
alt={alt}
className={cn("absolute inset-0 h-full w-full object-cover", className)}
style={{
clipPath: position === "left" ? leftClipPath : rightClipPath
}}
/>
);
};
const ImageComparisonSlider = ({
className,
children
}: {
className: string;
children?: React.ReactNode;
}) => {
const { motionSliderPosition } = useContext(ImageComparisonContext)!;
const left = useTransform(motionSliderPosition, (value) => `${value}%`);
return (
<motion.div
className={cn("absolute top-0 bottom-0 w-1 cursor-ew-resize", className)}
style={{
left
}}>
{children}
</motion.div>
);
};
export { ImageComparison, ImageComparisonImage, ImageComparisonSlider };
Update the import paths to match your project setup.
Usage
import {
ImageComparison,
ImageComparisonImage,
ImageComparisonSlider
} from "@/components/core/image-comparison";
export default function ImageComparisonBasic() {
return (
<ImageComparison className="aspect-16/9 w-full rounded-lg" enableHover>
<ImageComparisonImage
src="https://bundui-images.netlify.app/blog/01.jpg"
className="grayscale"
alt="Motion Primitives Dark"
position="left"
/>
<ImageComparisonImage
src="https://bundui-images.netlify.app/blog/01.jpg"
alt="Motion Primitives Light"
position="right"
/>
<ImageComparisonSlider className="w-0.5 bg-white/30 backdrop-blur-xs">
<div className="absolute top-1/2 left-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white"></div>
</ImageComparisonSlider>
</ImageComparison>
);
}
Props
Prop | Type | Default |
---|---|---|
springOptions? | SpringOptions | { bounce: 0, duration: 0 } |
enableHover? | boolean | false |
className? | string | undefined |
children? | React.ReactNode | - |
Floating Button
A floating button is a common UI component used in user interfaces. It typically appears as a button "floating" over other content, often positioned in a corner of the screen. This button allows users to quickly perform a key action.
Tilt Effect
Tilt Effect is an interactive component that creates a perspective effect by making buttons, cards or images respond to mouse movements.