Before After Slider
Drag handle to compare two views with a smooth wipe reveal
interactiveslidercomparebefore-afterdrag
Preview
Source Code
"use client";
import { useRef, useState, useCallback, useEffect, Children } from "react";
interface BeforeAfterSliderProps {
children: React.ReactNode;
initialPosition?: number;
handleColor?: string;
className?: string;
}
export default function BeforeAfterSlider({
children,
initialPosition = 50,
handleColor = "#fff",
className = "",
}: BeforeAfterSliderProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState(initialPosition);
const [isDragging, setIsDragging] = useState(false);
const childArray = Children.toArray(children);
const beforeContent = childArray[0];
const afterContent = childArray[1];
const updatePosition = useCallback(
(clientX: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = clientX - rect.left;
const percent = Math.max(0, Math.min(100, (x / rect.width) * 100));
setPosition(percent);
},
[]
);
useEffect(() => {
if (!isDragging) return;
const handleMove = (e: MouseEvent) => {
e.preventDefault();
updatePosition(e.clientX);
};
const handleTouchMove = (e: TouchEvent) => {
updatePosition(e.touches[0].clientX);
};
const handleUp = () => setIsDragging(false);
window.addEventListener("mousemove", handleMove);
window.addEventListener("mouseup", handleUp);
window.addEventListener("touchmove", handleTouchMove);
window.addEventListener("touchend", handleUp);
return () => {
window.removeEventListener("mousemove", handleMove);
window.removeEventListener("mouseup", handleUp);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handleUp);
};
}, [isDragging, updatePosition]);
return (
<div
ref={containerRef}
className={`relative select-none overflow-hidden rounded-xl ${className}`}
style={{ cursor: isDragging ? "ew-resize" : "default" }}
>
{/* After layer (full width, behind) */}
<div className="relative w-full">
{afterContent}
</div>
{/* Before layer (clipped) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${position}%` }}
>
<div
className="h-full"
style={{
width: containerRef.current
? `${containerRef.current.clientWidth}px`
: "100vw",
}}
>
{beforeContent}
</div>
</div>
{/* Handle */}
<div
className="absolute inset-y-0 z-10"
style={{ left: `${position}%`, transform: "translateX(-50%)" }}
>
{/* Vertical line */}
<div
className="h-full w-[2px]"
style={{ backgroundColor: handleColor }}
/>
{/* Drag handle circle */}
<div
className="absolute top-1/2 left-1/2 flex h-10 w-10 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize items-center justify-center rounded-full border-2 bg-gray-950/60 backdrop-blur-sm"
style={{ borderColor: handleColor }}
onMouseDown={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onTouchStart={() => setIsDragging(true)}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke={handleColor}
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 8l4 4-4 4" />
<path d="M6 8l-4 4 4 4" />
</svg>
</div>
</div>
</div>
);
}