Urban Dreams
Neon Nights
Desert Whispers
Installation
Install dependencies
npm i motion clsx tailwind-merge
For React 19 / Next.js 15 users, follow the following packages
For React 19 / Next.js 15 users, either use the --legacy-peer-deps
flag or use --force
while installation.
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/carousel.tsx
"use client";
import { IconArrowNarrowRight } from "@tabler/icons-react";
import { useState, useRef, useId, useEffect } from "react";
interface SlideData {
title: string;
button: string;
src: string;
}
interface SlideProps {
slide: SlideData;
index: number;
current: number;
handleSlideClick: (index: number) => void;
}
const Slide = ({ slide, index, current, handleSlideClick }: SlideProps) => {
const slideRef = useRef<HTMLLIElement>(null);
const xRef = useRef(0);
const yRef = useRef(0);
const frameRef = useRef<number>();
useEffect(() => {
const animate = () => {
if (!slideRef.current) return;
const x = xRef.current;
const y = yRef.current;
slideRef.current.style.setProperty("--x", `${x}px`);
slideRef.current.style.setProperty("--y", `${y}px`);
frameRef.current = requestAnimationFrame(animate);
};
frameRef.current = requestAnimationFrame(animate);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, []);
const handleMouseMove = (event: React.MouseEvent) => {
const el = slideRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
xRef.current = event.clientX - (r.left + Math.floor(r.width / 2));
yRef.current = event.clientY - (r.top + Math.floor(r.height / 2));
};
const handleMouseLeave = () => {
xRef.current = 0;
yRef.current = 0;
};
const imageLoaded = (event: React.SyntheticEvent<HTMLImageElement>) => {
event.currentTarget.style.opacity = "1";
};
const { src, button, title } = slide;
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}
style={{
transform:
current !== index
? "scale(0.98) rotateX(8deg)"
: "scale(1) rotateX(0deg)",
transition: "transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
transformOrigin: "bottom",
}}
>
<div
className="absolute top-0 left-0 w-full h-full bg-[#1D1F2F] rounded-[1%] overflow-hidden transition-all duration-150 ease-out"
style={{
transform:
current === index
? "translate3d(calc(var(--x) / 30), calc(var(--y) / 30), 0)"
: "none",
}}
>
<img
className="absolute inset-0 w-[120%] h-[120%] object-cover opacity-100 transition-opacity duration-600 ease-in-out"
style={{
opacity: current === index ? 1 : 0.5,
}}
alt={title}
src={src}
onLoad={imageLoaded}
loading="eager"
decoding="sync"
/>
{current === index && (
<div 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">
<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>
</div>
</article>
</li>
</div>
);
};
interface CarouselControlProps {
type: string;
title: string;
handleClick: () => void;
}
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}
>
<IconArrowNarrowRight className="text-neutral-600 dark:text-neutral-200" />
</button>
);
};
interface CarouselProps {
slides: SlideData[];
}
export function Carousel({ slides }: CarouselProps) {
const [current, setCurrent] = useState(0);
const handlePreviousClick = () => {
const previous = current - 1;
setCurrent(previous < 0 ? slides.length - 1 : previous);
};
const handleNextClick = () => {
const next = current + 1;
setCurrent(next === slides.length ? 0 : next);
};
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
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}
/>
))}
</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>
);
}
Props
Carousel Component Props
Prop | Type | Required | Description |
---|---|---|---|
slides | SlideData[] | Yes | Array of slide objects containing title, button text, and image source |
SlideData Interface
Property | Type | Required | Description |
---|---|---|---|
title | string | Yes | Title text for the slide |
button | string | Yes | Text to display on the slide's button |
src | string | Yes | Image source URL for the slide |
Slide Component Props
Prop | Type | Required | Description |
---|---|---|---|
slide | SlideData | Yes | Object containing the slide data |
index | number | Yes | Current index of the slide |
current | number | Yes | Index of the currently active slide |
handleSlideClick | (index: number) => void | Yes | Function to handle slide click events |
CarouselControl Component Props
Prop | Type | Required | Description |
---|---|---|---|
type | string | Yes | Type of control ("previous" or "next") |
title | string | Yes | Accessibility title for the control button |
handleClick | () => void | Yes | Function to handle control click events |
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
Excellent communication and professionalism from the start and
throughout. Happily and calmly accepted and entertained a few additional
out-of-scope requests as well. Good open-...
Henrik Söderlund
Former CTO at Creme Digital