Carousel Variants

High-impact presentation components powered by GSAP. From kinetic typography to fluid liquid distortions.

GPU Accelerated
Copy & Paste
01

Kinetic Carousel

A dynamic carousel that uses aggressive easing (`box-shadow`, `scale`) and blur filters to create a high-speed, futuristic feel.

Neon Nights

A cyberpunk journey through illuminated streets where technology meets decay.

Installation

npx shadcn@latest add https://aftershade.pages.dev/registry/kinetic-carousel.json

Source Code

"use client";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import gsap from "gsap";
import { ChevronRight, ChevronLeft } from "lucide-react";

interface CarouselItem {
    id: string;
    title: string;
    description: string;
    image: string;
    icon: React.ReactNode;
}

export interface KineticCarouselProps {
    items: CarouselItem[];
    navType?: "icons" | "arrows";
    autoPlay?: boolean;
    interval?: number;
    className?: string;
}

export function KineticCarousel({
    items,
    navType = "icons",
    autoPlay = false,
    interval = 5000,
    className
}: KineticCarouselProps) {
    const [activeIndex, setActiveIndex] = useState(0);
    const containerRef = useRef<HTMLDivElement>(null);
    const contentRef = useRef<HTMLDivElement>(null);
    const imageRef = useRef<HTMLDivElement>(null);
    const intervalRef = useRef<NodeJS.Timeout | null>(null);

    const nextSlide = useCallback(() => {
        setActiveIndex((prev) => (prev + 1) % items.length);
    }, [items.length]);

    const prevSlide = useCallback(() => {
        setActiveIndex((prev) => (prev - 1 + items.length) % items.length);
    }, [items.length]);

    useEffect(() => {
        if (autoPlay) {
            intervalRef.current = setInterval(nextSlide, interval);
        }
        return () => {
            if (intervalRef.current) clearInterval(intervalRef.current);
        };
    }, [autoPlay, interval, nextSlide]);

    useEffect(() => {
        if (!contentRef.current || !imageRef.current) return;

        const tl = gsap.timeline();

        tl.set(imageRef.current, {
            scale: 0.2,
            opacity: 0,
            transformOrigin: "bottom right",
            filter: "blur(10px)"
        });
        tl.set(contentRef.current.children, {
            y: 20,
            opacity: 0
        });

        tl.to(imageRef.current, {
            scale: 1,
            opacity: 1,
            filter: "blur(0px)",
            duration: 0.8,
            ease: "power4.out"
        })
            .to(contentRef.current.children, {
                y: 0,
                opacity: 1,
                duration: 0.5,
                stagger: 0.1,
                ease: "back.out(1.7)"
            }, "-=0.4");

    }, [activeIndex]);

    return (
        <div
            className={cn(
                "relative w-full aspect-[16/9] rounded-[3rem] bg-zinc-950 border border-white/5 overflow-hidden group shadow-2xl",
                className
            )}
            ref={containerRef}
        >
            {/* Active Slide Image */}
            <div
                key={items[activeIndex].id}
                ref={imageRef}
                className="absolute inset-0 z-0 bg-cover bg-center"
                style={{ backgroundImage: `url('${items[activeIndex].image}')` }}
            >
                <div className="absolute inset-0 bg-gradient-to-t from-zinc-950 via-zinc-950/40 to-transparent" />
            </div>

            {/* Content */}
            <div
                ref={contentRef}
                className="absolute bottom-12 left-12 z-10 max-w-xl space-y-4"
            >
                <h2 className="text-5xl font-black tracking-tighter text-white uppercase italic">
                    {items[activeIndex].title}
                </h2>
                <p className="text-xl text-zinc-400 font-medium leading-relaxed">
                    {items[activeIndex].description}
                </p>
            </div>

            {/* Navigation Layer */}
            {navType === "icons" ? (
                <div className="absolute bottom-12 right-12 z-20 flex gap-3 p-3 bg-black/40 backdrop-blur-xl rounded-[2.5rem] border border-white/10">
                    {items.map((item, index) => (
                        <button
                            key={item.id}
                            onClick={() => {
                                setActiveIndex(index);
                                if (intervalRef.current) clearInterval(intervalRef.current);
                            }}
                            className={cn(
                                "w-12 h-12 flex items-center justify-center rounded-2xl transition-all duration-500",
                                activeIndex === index
                                    ? "bg-primary-1 text-white scale-110 shadow-[0_0_20px_rgba(var(--primary-1-rgb),0.4)]"
                                    : "text-zinc-500 hover:text-zinc-200 hover:bg-white/5"
                            )}
                        >
                            {item.icon}
                        </button>
                    ))}
                </div>
            ) : (
                <div className="absolute bottom-12 right-12 z-20 flex gap-4">
                    <button
                        onClick={prevSlide}
                        className="w-14 h-14 flex items-center justify-center rounded-2xl bg-black/40 backdrop-blur-xl border border-white/10 text-white hover:bg-primary-1 transition-all"
                    >
                        <ChevronLeft className="h-6 w-6" />
                    </button>
                    <button
                        onClick={nextSlide}
                        className="w-14 h-14 flex items-center justify-center rounded-2xl bg-black/40 backdrop-blur-xl border border-white/10 text-white hover:bg-primary-1 transition-all"
                    >
                        <ChevronRight className="h-6 w-6" />
                    </button>
                </div>
            )}

            {/* Auto-play Progress Bar */}
            {autoPlay && (
                <div className="absolute bottom-0 left-0 h-1 bg-primary-1/30 w-full z-30">
                    <div
                        key={activeIndex}
                        className="h-full bg-primary-1 transition-all linear"
                        style={{
                            width: '100%',
                            animation: `carousel-progress ${interval}ms linear forwards`
                        }}
                    />
                </div>
            )}

            <style jsx>{`
        @keyframes carousel-progress {
            from { width: 0%; }
            to { width: 100%; }
        }
      `}</style>
        </div>
    );
}
02

Perspective Carousel

Uses CSS `perspective` and GSAP 3D transforms (`rotateX`, `rotateY`) to create depth.

Deep Space

The final frontier, vast and unexplored.

Installation

npx shadcn@latest add https://aftershade.pages.dev/registry/perspective-carousel.json
03

Liquid Carousel

Uses `clip-path` animations to create a fluid, growing circular reveal effect.

Oceanic

The deep blue depths of our world's oceans.

Installation

npx shadcn@latest add https://aftershade.pages.dev/registry/liquid-carousel.json
04

Blur Carousel

Cinematic reveal using `backdrop-filter` and scale.

Cinematic Variant

Velocity

Speed captured in a single frame of light.

Installation

npx shadcn@latest add https://aftershade.pages.dev/registry/blur-carousel.json