Command Palette

Search for a command to run...

Image Comparison

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

Motion Primitives DarkMotion Primitives Light

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motion/image-comparison.tsx and paste the given code.

"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 select-none overflow-hidden",
                    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 bottom-0 top-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.