Docs

Horizontal Scroll Carousel

A Horizontal Scroll Carousel is a UI component that lets users scroll sideways to browse through multiple items seamlessly.

Installation

Install dependencies

npm install framer-motion lucide-react

Run the following command

It will create a new file horizontal-scroll-carousel.tsx inside the components/mage-ui/carousel directory.

mkdir -p components/mage-ui/carousel && touch components/mage-ui/carousel/horizontal-scroll-carousel.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
import { motion, useTransform, useScroll, useMotionValue } from "framer-motion";
import { useRef, useEffect, useState } from "react";
 
const HorizontalScrollCarouselComponent = () => {
  return (
    <div className="bg-neutral-800">
      <div className="flex h-48 items-center justify-center">
        <span className="font-semibold uppercase text-neutral-500">
          Scroll down
        </span>
      </div>
      <HorizontalScrollCarousel />
      <div className="flex h-48 items-center justify-center">
        <span className="font-semibold uppercase text-neutral-500">
          Scroll up
        </span>
      </div>
    </div>
  );
};
 
const HorizontalScrollCarousel = () => {
  const targetRef = useRef<HTMLDivElement | null>(null);
  const carouselRef = useRef<HTMLDivElement | null>(null);
  const scrollX = useMotionValue(0);
  const [isScrolling, setIsScrolling] = useState(true); // State to manage scrolling
 
  const totalWidth = cards.length * 450; // 450px per card
  const maxScroll = -(totalWidth - window.innerWidth); // Max scroll distance
 
  // Handle wheel scroll for horizontal movement
  useEffect(() => {
    const handleWheel = (event: WheelEvent) => {
      if (targetRef.current && targetRef.current.contains(event.target as Node)) {
        event.preventDefault(); // Prevent vertical scroll while in carousel
        if (isScrolling) {
          const delta = event.deltaY * -0.5; // Adjust scroll speed
          const newX = Math.min(0, Math.max(maxScroll, scrollX.get() + delta));
          scrollX.set(newX);
        }
      }
    };
 
    window.addEventListener("wheel", handleWheel, { passive: false });
    return () => window.removeEventListener("wheel", handleWheel);
  }, [scrollX, maxScroll, isScrolling]);
 
  // Determine if scroll has reached the end
  const isScrollComplete = scrollX.get() <= maxScroll + 10; // Small buffer
 
  // Update scrolling state based on completion
  useEffect(() => {
    setIsScrolling(!isScrollComplete);
  }, [isScrollComplete]);
 
  return (
    <section ref={targetRef} className="relative h-screen bg-neutral-900">
      <div className="sticky top-0 flex h-screen flex-col items-center justify-center overflow-hidden">
        <motion.div
          ref={carouselRef}
          style={{ x: scrollX }}
          className="flex gap-4"
        >
          {cards.map((card) => (
            <Card card={card} key={card.id} />
          ))}
        </motion.div>
        <motion.div
          className="mt-4 text-neutral-500 font-semibold uppercase"
          initial={{ opacity: 0 }}
          animate={{ opacity: isScrollComplete ? 1 : 0 }}
          transition={{ duration: 0.3 }}
        >
          {isScrollComplete ? "End of Carousel - Scroll Down to Continue" : "Keep Scrolling Right"}
        </motion.div>
      </div>
    </section>
  );
};
 
const Card = ({ card }: { card: CardType }) => {
  return (
    <div
      key={card.id}
      className="group relative h-[450px] w-[350px] overflow-hidden bg-neutral-200"
    >
      <div
        style={{
          backgroundImage: `url(${card.url})`,
          backgroundSize: "cover",
          backgroundPosition: "center",
        }}
        className="absolute inset-0 z-0 transition-transform duration-300 group-hover:scale-110"
      ></div>
      <div className="absolute inset-0 z-10 grid place-content-center">
        <p className="bg-gradient-to-br from-white/20 to-white/0 p-8 text-6xl font-black uppercase text-white backdrop-blur-lg">
          {card.title}
        </p>
      </div>
    </div>
  );
};
 
export default HorizontalScrollCarouselComponent;
 
type CardType = {
  url: string;
  title: string;
  id: number;
};
 
const cards: CardType[] = [
  {
    url: "https://images.unsplash.com/photo-1635373670332-43ea883bb081?q=80&w=2781&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 1",
    id: 1,
  },
  {
    url: "https://images.unsplash.com/photo-1576174464184-fb78fe882bfd?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 2",
    id: 2,
  },
  {
    url: "https://images.unsplash.com/photo-1503751071777-d2918b21bbd9?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 3",
    id: 3,
  },
  {
    url: "https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=2609&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 4",
    id: 4,
  },
  {
    url: "https://images.unsplash.com/photo-1602212096437-d0af1ce0553e?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 5",
    id: 5,
  },
  {
    url: "https://images.unsplash.com/photo-1622313762347-3c09fe5f2719?q=80&w=2640&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    title: "Title 6",
    id: 6,
  },
];