Playground

Draggable Slider

October 2024

Draggable slider
Draggable slider
Draggable slider
Draggable slider
Draggable slider
Draggable slider
Drag/Scroll
"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);
        },
        []
    );

    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);
        };
    }, []);

    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;
Previous

Exclusion Navigation

Next

Tiles Background