Drag/Scroll
Draggable Slider
October 2024
"use client";
import React, {useCallback, useEffect, useRef} from "react";
const cardImages = [
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-5.jpeg?updatedAt=1727346713644",
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-1.jpeg?updatedAt=1727346713538",
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-4.jpeg?updatedAt=1727346713535",
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-2.jpeg?updatedAt=1727346713491",
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-6.jpg?updatedAt=1727346713457",
"https://ik.imagekit.io/khoaphan/playground/Draggable%20slider/image-3.jpeg?updatedAt=1727346713529",
];
const DraggableSlider = () => {
const vw = useRef(0);
const mouseDownX = useRef(0);
const prePercentage = useRef(0);
const nextPercentage = useRef(0);
const $slider = useRef<HTMLDivElement>(null);
const isMouseInside = useRef(false);
const updateSliderPosition = useCallback((percentage: number) => {
const images = $slider.current?.querySelectorAll("img");
if (!images) return;
nextPercentage.current = Math.max(Math.min(percentage, 0), -100);
$slider.current?.animate(
{
transform: `translateX(${nextPercentage.current}%) translateY(-50%)`,
},
{duration: 2000, fill: "forwards"}
);
images.forEach((image) => {
image.animate(
{
objectPosition: `${nextPercentage.current + 100}% center`,
},
{duration: 2000, fill: "forwards"}
);
});
}, []);
const handleOnMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
mouseDownX.current = event.clientX;
},
[]
);
const handleOnMouseMove = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (!mouseDownX.current) return;
const maxSlidingWidth = vw.current / 2;
const deltaX = mouseDownX.current - event.clientX;
const percentage = (deltaX / maxSlidingWidth) * -100;
updateSliderPosition(prePercentage.current + percentage);
},
[updateSliderPosition]
);
const handleOnMouseUp = useCallback(() => {
mouseDownX.current = 0;
prePercentage.current = nextPercentage.current;
}, []);
const handleOnMouseLeave = useCallback(() => {
mouseDownX.current = 0;
prePercentage.current = nextPercentage.current;
isMouseInside.current = false;
}, []);
const handleOnMouseEnter = useCallback(() => {
isMouseInside.current = true;
}, []);
const handleScroll = useCallback(
(event: WheelEvent) => {
if (!isMouseInside.current) return;
event.preventDefault();
const scrollSensitivity = 0.1;
const scrollDelta = event.deltaY * scrollSensitivity;
updateSliderPosition(nextPercentage.current - scrollDelta);
},
[updateSliderPosition]
);
useEffect(() => {
const updateViewWidth = () => {
vw.current = window.innerWidth;
};
updateViewWidth();
window.addEventListener("resize", updateViewWidth);
// Handle scroll
window.addEventListener("wheel", handleScroll, {passive: false});
return () => {
window.removeEventListener("resize", updateViewWidth);
window.removeEventListener("wheel", handleScroll);
};
}, [handleScroll]);
return (
<article className="grid min-h-96 place-items-center bg-white/5 sm:aspect-[4/3] sm:min-h-0">
<div
className="relative h-full w-full cursor-grab"
onMouseDown={handleOnMouseDown}
onMouseMove={handleOnMouseMove}
onMouseUp={handleOnMouseUp}
onMouseLeave={handleOnMouseLeave}
onMouseEnter={handleOnMouseEnter}>
<div
className="pointer-events-none absolute left-1/2 top-1/2 h-[50%] -translate-y-1/2 transition-transform duration-1000 will-change-transform"
ref={$slider}>
<div className="relative flex h-full gap-x-5">
{cardImages.map((image, index) => (
<div
key={index}
// aspect ratio here is the most important factor that can make the parallax effect
// the aspect ratio of the image needs to be BIGGER than the original aspect-ratio of the image to make the object-fit property works
// another alternative can be increase the size of the image and manipulate translateX property.
className="relative aspect-[1/3] h-full bg-white/5">
<img
className="pointer-events-none h-full w-full select-none object-cover object-[100%_50%]"
src={image}
loading="lazy"
alt="Draggable slider"
/>
<div className="absolute inset-0 bg-black/20"></div>
</div>
))}
</div>
</div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="white"
className="w-8 sm:w-[2vw]">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 4.5v15m7.5-7.5h-15"
/>
</svg>
</div>
<div className="absolute bottom-10 left-1/2 flex -translate-x-1/2 items-center gap-x-2 text-center font-poppins text-white">
<span className="inline-flex items-center gap-x-1">
Drag
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.05 4.575a1.575 1.575 0 1 0-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 0 1 3.15 0v1.5m-3.15 0 .075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 0 1 3.15 0V15M6.9 7.575a1.575 1.575 0 1 0-3.15 0v8.175a6.75 6.75 0 0 0 6.75 6.75h2.018a5.25 5.25 0 0 0 3.712-1.538l1.732-1.732a5.25 5.25 0 0 0 1.538-3.712l.003-2.024a.668.668 0 0 1 .198-.471 1.575 1.575 0 1 0-2.228-2.228 3.818 3.818 0 0 0-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0 1 16.35 15m.002 0h-.002"
/>
</svg>
</span>
/
<span className="inline-flex items-center gap-x-1">
Scroll
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-5">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
</svg>
</span>
</div>
</div>
</article>
);
};
export default DraggableSlider;