Docs
Carousel
Carousel
A customizable Carousel component.
Install the following dependencies:
pnpm add gsap react-icon
Make a file for cn function and match the import afterwards
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))
Make a file and copy paste this code in a file with name Carousel
"use client"
import { FaArrowRightLong } from "react-icons/fa6"
import { useState, useRef, useId, useEffect } from "react"
import { gsap } from "gsap"
import Link from "next/link"
interface CarouselProps {
slides: SlideData[]
}
interface CarouselControlProps {
type: string
title: string
handleClick: () => void
}
interface SlideData {
title: string
button: string
src: string
link: string
}
interface SlideProps {
slide: SlideData
index: number
current: number
handleSlideClick: (index: number) => void
parentRef: React.RefObject<HTMLUListElement>
}
const Carousel = ({ slides }: CarouselProps) => {
const [current, setCurrent] = useState(0)
const parentRef = useRef<HTMLUListElement>(null)
const handlePreviousClick = () => {
const previous = current - 1
const newCurrent = previous < 0 ? slides.length - 1 : previous
const parent = parentRef.current?.childNodes
if (!parent) return
const tl = gsap.timeline()
const prevSlide = parent[newCurrent].childNodes[0] as Element
// Animate active slide: 0.4 sec from { scale: 0.98, rotateX: 8 } to { scale: 1, rotateX: 0 }
tl.fromTo(
prevSlide,
{ scale: 0.98, rotateX: 8 },
{ scale: 1, rotateX: 0, duration: 0.4, ease: "power2.out" }
)
// Animate the translation of the slides container over 0.3 sec
const ulEl = parentRef.current
if (ulEl) {
gsap.to(ulEl, {
duration: 0.3,
x: `-${newCurrent * (100 / slides.length)}%`,
ease: "power2.out",
})
}
// Animate remaining slides to maintain their state
for (let i = 0; i < parent.length; i++) {
if (i !== newCurrent) {
const slideElement = (parent[i] as Element).childNodes[0] as Element
gsap.to(slideElement, {
scale: 0.98,
rotateX: 8,
duration: 0.4,
ease: "power2.out",
})
}
}
setCurrent(newCurrent)
}
const handleNextClick = () => {
const next = current + 1
const newCurrent = next === slides.length ? 0 : next
const parent = parentRef.current?.childNodes
if (!parent) return
const tl = gsap.timeline()
const nextSlide = parent[newCurrent].childNodes[0] as Element
// Animate active slide: 0.3 sec tween then hold for 0.7 sec
tl.fromTo(
nextSlide,
{ scale: 0.98, rotateX: 8 },
{ scale: 1, rotateX: 0, duration: 0.4, ease: "power2.out" }
)
// .to(nextSlide, { duration: 0.4 });
// Animate the translation of the slides container over 1 sec
const ulEl = parentRef.current
if (ulEl) {
gsap.to(ulEl, {
duration: 0.3,
x: `-${newCurrent * (100 / slides.length)}%`,
ease: "power2.out",
})
}
// Animate remaining slides to maintain their state
for (let i = 0; i < parent.length; i++) {
if (i !== newCurrent) {
const slideElement = (parent[i] as Element).childNodes[0] as Element
gsap.to(slideElement, {
scale: 0.98,
rotateX: 8,
duration: 0.4,
ease: "power2.out",
})
}
}
setCurrent(newCurrent)
}
const handleSlideClick = (index: number) => {
if (current !== index) {
setCurrent(index)
}
}
const id = useId()
return (
<div
className="relative w-[70vmin] h-[70vmin] mx-auto"
aria-labelledby={`carousel-heading-${id}`}
>
<ul
ref={parentRef}
className="absolute flex mx-[-4vmin] transition-transform duration-1000 ease-in-out"
style={{
transform: `translateX(-${current * (100 / slides.length)}%)`,
}}
>
{slides.map((slide, index) => (
<Slide
key={index}
slide={slide}
index={index}
current={current}
handleSlideClick={handleSlideClick}
parentRef={parentRef}
/>
))}
</ul>
<div className="absolute flex justify-center w-full top-[calc(100%+1rem)]">
<CarouselControl
type="previous"
title="Go to previous slide"
handleClick={handlePreviousClick}
/>
<CarouselControl
type="next"
title="Go to next slide"
handleClick={handleNextClick}
/>
</div>
</div>
)
}
export { Carousel }
const Slide = ({
slide,
index,
current,
handleSlideClick,
parentRef,
}: SlideProps) => {
const slideRef = useRef<HTMLLIElement>(null)
const xRef = useRef(0)
const yRef = useRef(0)
const imageRef = useRef<HTMLImageElement>(null)
const divRef = useRef<HTMLDivElement>(null)
const divRef2 = useRef<HTMLDivElement>(null)
const { src, button, title, link } = slide
useEffect(() => {
if (current !== index) {
gsap.to(slideRef.current, {
scale: 0.98,
rotateX: 8,
duration: 0.5,
ease: "power2.out",
})
gsap.to(imageRef.current, {
opacity: 0.5,
duration: 0.6,
})
}
}, [current, index])
const handleMouseMove = (event: React.MouseEvent) => {
if (current !== index) return
const el = slideRef.current
const img = divRef2.current
if (!el) return
const rect = el.getBoundingClientRect()
const x = event.clientX - (rect.left + rect.width / 2)
const y = event.clientY - (rect.top + rect.height / 2)
xRef.current = x / 20
yRef.current = y / 20
gsap.to(img, {
x: xRef.current,
y: yRef.current,
duration: 0.2,
ease: "power2.out",
})
}
const handleMouseLeave = () => {
const img = divRef2.current
if (!img) return
gsap.to(img, {
x: 0,
y: 0,
duration: 0.3,
ease: "power2.out",
})
}
return (
<div className="[perspective:1200px] [transform-style:preserve-3d]">
<li
ref={slideRef}
className="flex flex-1 flex-col items-center justify-center relative text-center text-white opacity-100 transition-all duration-300 ease-in-out w-[70vmin] h-[70vmin] mx-[4vmin] z-10"
onClick={() => handleSlideClick(index)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<div
ref={divRef2}
className="absolute top-0 left-0 w-full h-full bg-[#1D1F2F] rounded-[1%] transition-all duration-150 ease-out"
>
<img
ref={imageRef}
className="absolute h-full w-full inset-0 rounded-[1%] object-cover opacity-100 transition-opacity duration-600 ease-in-out"
alt={title}
src={src}
loading="eager"
decoding="sync"
/>
{current === index && (
<div
ref={divRef}
className="absolute inset-0 bg-black/30 transition-all duration-1000"
/>
)}
</div>
<article
className={`relative p-[4vmin] transition-opacity duration-1000 ease-in-out ${
current === index ? "opacity-100 visible" : "opacity-0 invisible"
}`}
>
<h2 className="text-lg md:text-2xl lg:text-4xl font-semibold relative">
{title}
</h2>
<div className="flex justify-center">
<Link href={link}>
<button className="mt-6 px-4 py-2 w-fit mx-auto sm:text-sm text-black bg-white h-12 border border-transparent text-xs flex justify-center items-center rounded-2xl hover:shadow-lg transition duration-200 shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]">
{button}
</button>
</Link>
</div>
</article>
</li>
</div>
)
}
const CarouselControl = ({
type,
title,
handleClick,
}: CarouselControlProps) => {
return (
<button
className={`w-10 h-10 flex items-center mx-2 justify-center bg-neutral-200 dark:bg-neutral-800 border-3 border-transparent rounded-full focus:border-[#6D64F7] focus:outline-none hover:-translate-y-0.5 active:translate-y-0.5 transition duration-200 ${
type === "previous" ? "rotate-180" : ""
}`}
title={title}
onClick={handleClick}
>
<FaArrowRightLong className="text-neutral-600 dark:text-neutral-200" />
</button>
)
}
Provide an array of items with the following properties: id, name, designation, and imgUrl.
const slideData = [
{
title: "Mystic Mountains",
button: "Explore Component",
src: "https://images.unsplash.com/photo-1494806812796-244fe51b774d?q=80&w=3534&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
link:"https://www.unizoy.com/"
},
{
title: "Urban Dreams",
button: "Explore Component",
src: "https://images.unsplash.com/photo-1518710843675-2540dd79065c?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
link:"https://www.unizoy.com/"
},
...
...
...
];
If Image is not Visible,then modify NextConfig
const nextConfig: NextConfig = {
/_ config options here _/
images: {
domains: ["images.unsplash.com"], // Add Unsplash domain here
},
};
export default nextConfig;
Credit: This component is inspired by Aceternity.