Carousel
A carousel with motion and swipe built using Embla. Built with Tailwind CSS. Copy-paste ready.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
export function CarouselDemo() {
return (
<Carousel className="w-full max-w-48 sm:max-w-xs">
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="font-semibold text-4xl">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/carousel
Examples
Multiple Items
Shows multiple slides at once using fractional basis — each item takes half or a third of the track width, letting users see what's coming next.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
export function CarouselSize() {
return (
<Carousel
className="w-full max-w-48 sm:max-w-xs md:max-w-sm"
opts={{
align: "start",
}}
>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem className="basis-1/2 lg:basis-1/3" key={index}>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="font-semibold text-3xl">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}
Custom Spacing
Reduces the gap between slides by overriding the default padding, useful for tighter layouts where breathing room should be minimal.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
export function CarouselSpacing() {
return (
<Carousel className="w-full max-w-48 sm:max-w-xs md:max-w-sm">
<CarouselContent className="-ml-1">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem className="basis-1/2 pl-1 lg:basis-1/3" key={index}>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="font-semibold text-2xl">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}
Vertical Orientation
Switches the scroll axis to vertical via orientation="vertical" — suited for feeds, timelines, or any layout that naturally flows top-to-bottom.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
export function CarouselOrientation() {
return (
<Carousel
className="w-full max-w-xs"
opts={{
align: "start",
}}
orientation="vertical"
>
<CarouselContent className="-mt-1 h-67.5">
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem className="basis-1/2 pt-1" key={index}>
<div className="p-1">
<Card>
<CardContent className="flex items-center justify-center p-6">
<span className="font-semibold text-3xl">{index + 1}</span>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}
Autoplay with Indicators
Auto-advances every 2.5 s using the Embla API directly via setInterval, with pill-shaped dot indicators below that expand to show the active slide and allow manual jump.
Build fast
Ship production-ready components today
Stay consistent
One design system, every surface
Scale with ease
From prototype to enterprise
Fully accessible
WCAG 2.1 AA out of the box
Open source
Free forever, community-driven
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const SLIDES = [
{
bg: "from-violet-500 to-purple-700",
label: "Build fast",
sub: "Ship production-ready components today",
},
{
bg: "from-sky-500 to-blue-700",
label: "Stay consistent",
sub: "One design system, every surface",
},
{
bg: "from-emerald-500 to-teal-700",
label: "Scale with ease",
sub: "From prototype to enterprise",
},
{
bg: "from-rose-500 to-pink-700",
label: "Fully accessible",
sub: "WCAG 2.1 AA out of the box",
},
{
bg: "from-amber-500 to-orange-600",
label: "Open source",
sub: "Free forever, community-driven",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
React.useEffect(() => {
if (!emblaApi) return;
const id = setInterval(() => emblaApi.scrollNext(), 2500);
return () => clearInterval(id);
}, [emblaApi]);
return (
<div className="w-full max-w-sm space-y-3">
<div className="overflow-hidden rounded-2xl" ref={emblaRef}>
<div className="flex">
{SLIDES.map((slide, i) => (
<div className="min-w-0 shrink-0 grow-0 basis-full" key={i}>
<div
className={cn(
"flex h-48 flex-col items-center justify-center rounded-2xl bg-linear-to-br text-white",
slide.bg,
)}
>
<p className="font-bold text-xl">{slide.label}</p>
<p className="mt-1 text-sm opacity-80">{slide.sub}</p>
</div>
</div>
))}
</div>
</div>
<div className="flex justify-center gap-1.5">
{SLIDES.map((_, i) => (
<button
aria-label={`Go to slide ${i + 1}`}
className={cn(
"h-1.5 rounded-full transition-all duration-300",
i === current ? "w-6 bg-primary" : "w-1.5 bg-muted-foreground/30",
)}
key={i}
onClick={() => emblaApi?.scrollTo(i)}
type="button"
/>
))}
</div>
</div>
);
}
Testimonials
Single-slide carousel for customer quotes — each card shows a star rating, a pull quote, and an avatar with name and role; loops infinitely with arrow controls.
“Cut our design-system setup from weeks to a single afternoon. The component quality is outstanding.”
Alice Park
Frontend Lead, Vercel
“We ship across 3 products with cnippet. Consistent, accessible, and easy to extend — exactly what we needed.”
Ben Torres
CTO, Linear
“The Figma-to-code fidelity is remarkable. My team ships faster because they trust the components.”
Celia Kim
Product Designer, Figma
“Best component library I've used. Open source, well-documented, and it just works.”
Dan Moss
Indie Developer
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import { StarIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const TESTIMONIALS = [
{
initials: "AP",
name: "Alice Park",
quote:
"Cut our design-system setup from weeks to a single afternoon. The component quality is outstanding.",
rating: 5,
role: "Frontend Lead, Vercel",
},
{
initials: "BT",
name: "Ben Torres",
quote:
"We ship across 3 products with cnippet. Consistent, accessible, and easy to extend — exactly what we needed.",
rating: 5,
role: "CTO, Linear",
},
{
initials: "CK",
name: "Celia Kim",
quote:
"The Figma-to-code fidelity is remarkable. My team ships faster because they trust the components.",
rating: 5,
role: "Product Designer, Figma",
},
{
initials: "DM",
name: "Dan Moss",
quote:
"Best component library I've used. Open source, well-documented, and it just works.",
rating: 5,
role: "Indie Developer",
},
];
export default function Particle() {
return (
<Carousel className="w-full max-w-sm" opts={{ loop: true }}>
<CarouselContent>
{TESTIMONIALS.map((t, i) => (
<CarouselItem key={i}>
<Card>
<CardContent className="space-y-4">
<div className="flex gap-0.5">
{Array.from({ length: t.rating }).map((_, j) => (
<StarIcon
className="size-4 fill-amber-400 text-amber-400"
key={j}
/>
))}
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
“{t.quote}”
</p>
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback className="text-xs">
{t.initials}
</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold text-sm">{t.name}</p>
<p className="text-muted-foreground text-xs">{t.role}</p>
</div>
</div>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}
Product Showcase
Multi-item carousel showing product cards with an image area, name, price, badge, and an Add to Cart button — aligned to start so partial next-slide is always visible.
Mechanical Keyboard
Bestseller$149
Ergonomic Mouse
New$89
USB-C Hub 7-in-1
Sale$59
4K Webcam
Popular$199
LED Desk Lamp
Sale$45
"use client";
import {
ArrowLeftIcon,
ArrowRightIcon,
CameraIcon,
KeyboardIcon,
MouseIcon,
PlugIcon,
ShoppingCartIcon,
SunIcon,
} from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
const PRODUCTS = [
{
badge: "Bestseller",
bg: "bg-violet-100 dark:bg-violet-950",
icon: KeyboardIcon,
iconColor: "text-violet-500 dark:text-violet-400",
id: 1,
name: "Mechanical Keyboard",
price: "$149",
},
{
badge: "New",
bg: "bg-sky-100 dark:bg-sky-950",
icon: MouseIcon,
iconColor: "text-sky-500 dark:text-sky-400",
id: 2,
name: "Ergonomic Mouse",
price: "$89",
},
{
badge: "Sale",
bg: "bg-emerald-100 dark:bg-emerald-950",
icon: PlugIcon,
iconColor: "text-emerald-500 dark:text-emerald-400",
id: 3,
name: "USB-C Hub 7-in-1",
price: "$59",
},
{
badge: "Popular",
bg: "bg-rose-100 dark:bg-rose-950",
icon: CameraIcon,
iconColor: "text-rose-500 dark:text-rose-400",
id: 4,
name: "4K Webcam",
price: "$199",
},
{
badge: "Sale",
bg: "bg-amber-100 dark:bg-amber-950",
icon: SunIcon,
iconColor: "text-amber-500 dark:text-amber-400",
id: 5,
name: "LED Desk Lamp",
price: "$45",
},
];
export default function Particle() {
const [api, setApi] = React.useState<CarouselApi>();
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(true);
React.useEffect(() => {
if (!api) return;
const update = () => {
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
};
api.on("select", update);
api.on("reInit", update);
update();
return () => {
api.off("select", update);
api.off("reInit", update);
};
}, [api]);
return (
<div className="w-full max-w-xs space-y-3">
<Carousel className="w-full" opts={{ align: "start" }} setApi={setApi}>
<CarouselContent>
{PRODUCTS.map((p) => (
<CarouselItem className="basis-3/4 sm:basis-2/3" key={p.id}>
<Card className="overflow-hidden">
<div
className={cn("flex h-36 items-center justify-center", p.bg)}
>
<p.icon
aria-hidden="true"
className={cn("size-14", p.iconColor)}
strokeWidth={1.25}
/>
</div>
<CardContent className="space-y-2 p-3">
<div className="flex items-start gap-2">
<p className="flex-1 font-medium text-sm leading-snug">
{p.name}
</p>
<Badge className="shrink-0" size="sm" variant="secondary">
{p.badge}
</Badge>
</div>
<p className="font-bold">{p.price}</p>
<Button className="w-full" size="sm">
<ShoppingCartIcon />
Add to Cart
</Button>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="flex justify-center gap-2">
<Button
className="size-8 rounded-full"
disabled={!canScrollPrev}
onClick={() => api?.scrollPrev()}
size="icon"
variant="outline"
>
<ArrowLeftIcon className="size-4" />
<span className="sr-only">Previous slide</span>
</Button>
<Button
className="size-8 rounded-full"
disabled={!canScrollNext}
onClick={() => api?.scrollNext()}
size="icon"
variant="outline"
>
<ArrowRightIcon className="size-4" />
<span className="sr-only">Next slide</span>
</Button>
</div>
</div>
);
}
Onboarding Steps
Step-by-step wizard built on raw Embla with drag disabled — a progress bar and "Step N of M" label update as the user moves through each step using labeled Back / Next buttons.
Welcome aboard
Let's get your workspace set up in just a few steps.
Choose a theme
Pick a color palette that matches your brand identity.
Pick your components
Select the UI building blocks your project needs.
Install via CLI
Run a single command to add components to your project.
You're all set!
Start building — your design system is ready to ship.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import {
CheckIcon,
CodeIcon,
LayersIcon,
PaletteIcon,
RocketIcon,
ZapIcon,
} from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
const STEPS = [
{
desc: "Let's get your workspace set up in just a few steps.",
icon: RocketIcon,
title: "Welcome aboard",
},
{
desc: "Pick a color palette that matches your brand identity.",
icon: PaletteIcon,
title: "Choose a theme",
},
{
desc: "Select the UI building blocks your project needs.",
icon: LayersIcon,
title: "Pick your components",
},
{
desc: "Run a single command to add components to your project.",
icon: CodeIcon,
title: "Install via CLI",
},
{
desc: "Start building — your design system is ready to ship.",
icon: ZapIcon,
title: "You're all set!",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({ watchDrag: false });
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
const isLast = current === STEPS.length - 1;
return (
<div className="w-full max-w-sm space-y-4 rounded-xl border bg-background p-6">
<div className="space-y-1.5">
<div className="flex items-center justify-between text-muted-foreground text-xs">
<span>Getting started</span>
<span>
Step {current + 1} of {STEPS.length}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{ width: `${((current + 1) / STEPS.length) * 100}%` }}
/>
</div>
</div>
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{STEPS.map((step, i) => {
const Icon = step.icon;
return (
<div className="min-w-0 shrink-0 grow-0 basis-full" key={i}>
<div className="flex flex-col items-center gap-3 py-6 text-center">
<div className="flex size-14 items-center justify-center rounded-full bg-primary/10">
<Icon aria-hidden="true" className="size-7 text-primary" />
</div>
<div>
<p className="font-semibold">{step.title}</p>
<p className="mt-1 text-muted-foreground text-sm">
{step.desc}
</p>
</div>
</div>
</div>
);
})}
</div>
</div>
<div className="flex gap-2">
<Button
className="flex-1"
disabled={current === 0}
onClick={() => emblaApi?.scrollPrev()}
variant="outline"
>
Back
</Button>
<Button
className="flex-1"
onClick={() => !isLast && emblaApi?.scrollNext()}
>
{isLast ? (
<>
<CheckIcon />
Finish
</>
) : (
"Next"
)}
</Button>
</div>
</div>
);
}
Team Members
Peek-style multi-item carousel for team cards — each card shows an avatar, name, role, department badge, and links; the partial next card signals more to explore.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import {
ArrowLeftIcon,
ArrowRightIcon,
GitBranch,
GlobeIcon,
} from "lucide-react";
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
const TEAM = [
{
dept: "Design",
github: "@alicepark",
initials: "AP",
name: "Alice Park",
role: "Lead Designer",
site: "alicepark.io",
},
{
dept: "Engineering",
github: "@bentorres",
initials: "BT",
name: "Ben Torres",
role: "Fullstack Eng.",
site: "bentorres.dev",
},
{
dept: "Product",
github: "@celiak",
initials: "CK",
name: "Celia Kim",
role: "Product Manager",
site: "celiakim.co",
},
{
dept: "Engineering",
github: "@danmoss",
initials: "DM",
name: "Dan Moss",
role: "Frontend Eng.",
site: "danmoss.dev",
},
{
dept: "Design",
github: "@evaruiz",
initials: "ER",
name: "Eva Ruiz",
role: "UX Researcher",
site: "evaruiz.design",
},
];
export default function Particle() {
const [api, setApi] = React.useState<CarouselApi>();
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(true);
React.useEffect(() => {
if (!api) return;
const update = () => {
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
};
api.on("select", update);
api.on("reInit", update);
update();
return () => {
api.off("select", update);
api.off("reInit", update);
};
}, [api]);
return (
<div className="w-full max-w-xs space-y-3">
<Carousel className="w-full" opts={{ align: "start" }} setApi={setApi}>
<CarouselContent>
{TEAM.map((member, i) => (
<CarouselItem className="basis-4/5 sm:basis-3/4" key={i}>
<Card>
<CardContent className="flex flex-col items-center gap-3 text-center">
<Avatar className="size-14">
<AvatarFallback className="text-base">
{member.initials}
</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">{member.name}</p>
<p className="text-muted-foreground text-sm">
{member.role}
</p>
</div>
<Badge variant="secondary">{member.dept}</Badge>
<div className="flex gap-3">
<a
className="flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground"
href="#"
>
<GitBranch aria-hidden="true" className="size-3.5" />
{member.github}
</a>
<a
className="flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground"
href="#"
>
<GlobeIcon aria-hidden="true" className="size-3.5" />
{member.site}
</a>
</div>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="flex justify-center gap-2">
<Button
className="size-8 rounded-full"
disabled={!canScrollPrev}
onClick={() => api?.scrollPrev()}
size="icon"
variant="outline"
>
<ArrowLeftIcon className="size-4" />
<span className="sr-only">Previous</span>
</Button>
<Button
className="size-8 rounded-full"
disabled={!canScrollNext}
onClick={() => api?.scrollNext()}
size="icon"
variant="outline"
>
<ArrowRightIcon className="size-4" />
<span className="sr-only">Next</span>
</Button>
</div>
</div>
);
}
Gallery with Thumbnails
Main carousel controlled programmatically via setApi — clicking a thumbnail scrolls the main view to that image, and the thumbnail strip stays in sync as the user swipes.
Mountain Sunset
Ocean Waves
Forest Path
City Lights
Desert Dunes
Northern Lights
"use client";
import useEmblaCarousel from "embla-carousel-react";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
const GALLERY = [
{ bg: "from-violet-400 to-purple-600", id: 1, label: "Mountain Sunset" },
{ bg: "from-sky-400 to-blue-600", id: 2, label: "Ocean Waves" },
{ bg: "from-emerald-400 to-teal-600", id: 3, label: "Forest Path" },
{ bg: "from-rose-400 to-pink-600", id: 4, label: "City Lights" },
{ bg: "from-amber-400 to-orange-500", id: 5, label: "Desert Dunes" },
{ bg: "from-indigo-400 to-cyan-500", id: 6, label: "Northern Lights" },
];
export default function Particle() {
const [api, setApi] = React.useState<CarouselApi>();
const [thumbRef, thumbApi] = useEmblaCarousel({
containScroll: "keepSnaps",
dragFree: true,
});
const [selected, setSelected] = React.useState(0);
React.useEffect(() => {
if (!api) return;
const onSelect = () => {
const idx = api.selectedScrollSnap();
setSelected(idx);
thumbApi?.scrollTo(idx);
};
api.on("select", onSelect);
return () => {
api.off("select", onSelect);
};
}, [api, thumbApi]);
return (
<div className="w-full max-w-sm space-y-2">
<Carousel className="w-full" setApi={setApi}>
<CarouselContent>
{GALLERY.map((item) => (
<CarouselItem key={item.id}>
<div
className={cn(
"relative flex h-52 flex-col items-center justify-end rounded-xl bg-linear-to-br pb-4 text-white",
item.bg,
)}
>
<p className="font-medium text-sm drop-shadow">{item.label}</p>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="overflow-hidden" ref={thumbRef}>
<div className="flex gap-2 px-px py-1">
{GALLERY.map((item, i) => (
<button
aria-label={`View ${item.label}`}
className={cn(
"shrink-0 rounded-lg transition-all duration-200",
selected === i
? "ring-2 ring-primary ring-offset-2"
: "opacity-50 hover:opacity-80",
)}
key={item.id}
onClick={() => api?.scrollTo(i)}
type="button"
>
<div
className={cn("h-12 w-16 rounded-lg bg-linear-to-br", item.bg)}
/>
</button>
))}
</div>
</div>
</div>
);
}
Testimonials
A looping testimonial carousel with a quote icon, reviewer name, role, and avatar initials. Dot indicators sync to the current slide and act as navigation buttons.
Dropped ui-cnippet into our Next.js app in under an hour. The components are polished and the DX is excellent.
Alex Kim
CTO, Launchpad
Finally a library that treats accessibility as a first-class feature, not an afterthought.
Sara Reyes
Lead Designer, Craft
The Tailwind integration is seamless. I was able to match our brand without touching any source files.
Marcus Ng
Frontend Engineer, Orbit
Honestly the best component library I've used. Shadcn-compatible and actively maintained.
Jade Park
Indie Developer
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import { QuoteIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const TESTIMONIALS = [
{
avatar: "AK",
name: "Alex Kim",
quote:
"Dropped ui-cnippet into our Next.js app in under an hour. The components are polished and the DX is excellent.",
role: "CTO, Launchpad",
},
{
avatar: "SR",
name: "Sara Reyes",
quote:
"Finally a library that treats accessibility as a first-class feature, not an afterthought.",
role: "Lead Designer, Craft",
},
{
avatar: "MN",
name: "Marcus Ng",
quote:
"The Tailwind integration is seamless. I was able to match our brand without touching any source files.",
role: "Frontend Engineer, Orbit",
},
{
avatar: "JP",
name: "Jade Park",
quote:
"Honestly the best component library I've used. Shadcn-compatible and actively maintained.",
role: "Indie Developer",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
return (
<div className="w-full max-w-sm space-y-4">
<div className="overflow-hidden rounded-xl" ref={emblaRef}>
<div className="flex">
{TESTIMONIALS.map((t, i) => (
<div className="min-w-0 shrink-0 grow-0 basis-full" key={i}>
<div className="flex flex-col gap-4 rounded-xl border bg-card p-6">
<QuoteIcon className="size-5 text-primary" />
<p className="text-sm leading-relaxed">{t.quote}</p>
<div className="flex items-center gap-3">
<div className="flex size-9 items-center justify-center rounded-full bg-primary font-bold text-[11px] text-primary-foreground">
{t.avatar}
</div>
<div>
<p className="font-semibold text-sm">{t.name}</p>
<p className="text-muted-foreground text-xs">{t.role}</p>
</div>
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex justify-center gap-1.5">
{TESTIMONIALS.map((_, i) => (
<button
aria-label={`Go to slide ${i + 1}`}
className={cn(
"h-1.5 rounded-full transition-all duration-300",
i === current ? "w-6 bg-primary" : "w-1.5 bg-muted-foreground/30",
)}
key={i}
onClick={() => emblaApi?.scrollTo(i)}
type="button"
/>
))}
</div>
</div>
);
}
Pricing Plans
A single-item carousel through Free, Pro, and Enterprise plans with arrow navigation. The Pro plan card is visually highlighted with a primary ring.
- · 5 projects
- · 2 GB storage
- · Community support
- · Unlimited projects
- · 50 GB storage
- · Priority support
- · Team collaboration
- · Custom limits
- · SSO & SAML
- · SLA guarantee
- · Dedicated CSM
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
const PRICING = [
{
cta: "Get started",
features: ["5 projects", "2 GB storage", "Community support"],
highlight: false,
name: "Free",
price: "$0",
},
{
cta: "Start trial",
features: [
"Unlimited projects",
"50 GB storage",
"Priority support",
"Team collaboration",
],
highlight: true,
name: "Pro",
price: "$12",
},
{
cta: "Contact us",
features: ["Custom limits", "SSO & SAML", "SLA guarantee", "Dedicated CSM"],
highlight: false,
name: "Enterprise",
price: "Custom",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({ startIndex: 1 });
const [current, setCurrent] = React.useState(1);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
return (
<div className="w-full max-w-xs space-y-3">
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{PRICING.map((plan, i) => (
<div className="min-w-0 shrink-0 grow-0 basis-full px-1" key={i}>
<div
className={cn(
"flex flex-col gap-4 rounded-xl border p-5",
plan.highlight && "border-primary ring-2 ring-primary",
)}
>
<div className="flex items-center justify-between">
<span className="font-semibold text-sm">{plan.name}</span>
{plan.highlight && <Badge size="sm">Popular</Badge>}
</div>
<div className="font-bold text-2xl">
{plan.price}
<span className="font-normal text-muted-foreground text-sm">
{plan.price !== "Custom" ? "/mo" : ""}
</span>
</div>
<ul className="flex flex-col gap-1.5">
{plan.features.map((f) => (
<li className="text-muted-foreground text-xs" key={f}>
· {f}
</li>
))}
</ul>
<button
className={cn(
"w-full rounded-md py-2 font-medium text-sm transition-colors",
plan.highlight
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "border hover:bg-muted",
)}
type="button"
>
{plan.cta}
</button>
</div>
</div>
))}
</div>
</div>
<div className="flex items-center justify-center gap-3">
<button
aria-label="Previous"
className="rounded-full border p-1.5 hover:bg-muted disabled:opacity-30"
disabled={current === 0}
onClick={() => emblaApi?.scrollPrev()}
type="button"
>
<ChevronLeftIcon className="size-4" />
</button>
<span className="text-muted-foreground text-xs">
{current + 1} / {PRICING.length}
</span>
<button
aria-label="Next"
className="rounded-full border p-1.5 hover:bg-muted disabled:opacity-30"
disabled={current === PRICING.length - 1}
onClick={() => emblaApi?.scrollNext()}
type="button"
>
<ChevronRightIcon className="size-4" />
</button>
</div>
</div>
);
}
Media Gallery
A carousel of labeled gradient slides with a slide counter and dot progress bar below, using the built-in CarouselPrevious / CarouselNext controls.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const MEDIA = [
{
bg: "from-violet-400 to-indigo-600",
caption: "Brand identity refresh",
type: "Image",
},
{
bg: "from-sky-400 to-cyan-600",
caption: "Product launch 2026",
type: "Video",
},
{
bg: "from-rose-400 to-pink-600",
caption: "Social media kit",
type: "Image",
},
{
bg: "from-amber-400 to-orange-500",
caption: "Campaign assets",
type: "Image",
},
{ bg: "from-emerald-400 to-teal-600", caption: "Demo reel", type: "Video" },
];
export default function Particle() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [count, setCount] = React.useState(0);
React.useEffect(() => {
if (!api) return;
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap());
api.on("select", () => setCurrent(api.selectedScrollSnap()));
}, [api]);
return (
<div className="w-full max-w-sm space-y-3">
<Carousel className="w-full" setApi={setApi}>
<CarouselContent>
{MEDIA.map((item, i) => (
<CarouselItem key={i}>
<div
className={cn(
"relative flex h-48 items-end rounded-xl bg-linear-to-br p-4",
item.bg,
)}
>
<div className="flex flex-col gap-0.5">
<span className="w-fit rounded-md bg-black/30 px-2 py-0.5 font-medium text-[10px] text-white">
{item.type}
</span>
<span className="font-semibold text-sm text-white drop-shadow">
{item.caption}
</span>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div className="flex items-center justify-between px-1 text-muted-foreground text-xs">
<span>
{current + 1} of {count}
</span>
<div className="flex gap-1">
{MEDIA.map((_, i) => (
<span
className={cn(
"block h-1 rounded-full transition-all",
i === current
? "w-4 bg-primary"
: "w-1.5 bg-muted-foreground/30",
)}
key={i}
/>
))}
</div>
</div>
</div>
);
}
Changelog
A drag-free multi-card carousel scrolling through release notes — each card shows a version tag, date, and a bullet list of changes.
- · Redesigned command palette
- · New chart variants
- · Dark mode improvements
- · Base UI migration complete
- · New autocomplete component
- · Bug fixes
- · Carousel with thumbnails
- · Accordion multi-mode
- · Performance improvements
- · Initial stable release
- · 30+ components
- · CLI installer
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const CHANGELOG = [
{
date: "Jun 2026",
items: [
"Redesigned command palette",
"New chart variants",
"Dark mode improvements",
],
tag: "v2.4.0",
tagColor: "bg-emerald-500",
},
{
date: "Apr 2026",
items: [
"Base UI migration complete",
"New autocomplete component",
"Bug fixes",
],
tag: "v2.3.0",
tagColor: "bg-blue-500",
},
{
date: "Feb 2026",
items: [
"Carousel with thumbnails",
"Accordion multi-mode",
"Performance improvements",
],
tag: "v2.2.0",
tagColor: "bg-violet-500",
},
{
date: "Dec 2025",
items: ["Initial stable release", "30+ components", "CLI installer"],
tag: "v2.0.0",
tagColor: "bg-amber-500",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({
align: "start",
dragFree: true,
});
const [selected, setSelected] = React.useState(0);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setSelected(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
return (
<div className="w-full max-w-sm space-y-3">
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex gap-3">
{CHANGELOG.map((release, i) => (
<div
className="min-w-55 shrink-0 rounded-xl border bg-card p-4"
key={i}
>
<div className="mb-3 flex items-center justify-between">
<span
className={cn(
"rounded-full px-2 py-0.5 font-bold text-[10px] text-white",
release.tagColor,
)}
>
{release.tag}
</span>
<span className="text-muted-foreground text-xs">
{release.date}
</span>
</div>
<ul className="flex flex-col gap-1.5">
{release.items.map((item) => (
<li className="text-xs" key={item}>
· {item}
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div className="flex justify-center gap-1.5">
{CHANGELOG.map((_, i) => (
<button
aria-label={`Scroll to ${i + 1}`}
className={cn(
"h-1.5 rounded-full transition-all",
i === selected
? "w-5 bg-primary"
: "w-1.5 bg-muted-foreground/30",
)}
key={i}
onClick={() => emblaApi?.scrollTo(i)}
type="button"
/>
))}
</div>
</div>
);
}
Feature Highlights
A slide-by-slide feature tour with an emoji icon, title, and description per slide. Dot indicators and Prev / Next buttons control navigation, with the last slide showing a "Done" checkmark.
Instant CLI install
Browse, copy, and install 50+ production-ready components with a single command.
Accessibility first
Every component meets WCAG 2.1 AA, with full keyboard navigation built in.
Fully themeable
Powered by Tailwind CSS — override any style without fighting specificity wars.
Headless foundation
Built on Base UI primitives so unstyled logic stays separate from visual styles.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import useEmblaCarousel from "embla-carousel-react";
import { CheckIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const FEATURES = [
{
bg: "bg-violet-50 dark:bg-violet-950/40",
description:
"Browse, copy, and install 50+ production-ready components with a single command.",
icon: "⚡",
title: "Instant CLI install",
},
{
bg: "bg-sky-50 dark:bg-sky-950/40",
description:
"Every component meets WCAG 2.1 AA, with full keyboard navigation built in.",
icon: "♿",
title: "Accessibility first",
},
{
bg: "bg-emerald-50 dark:bg-emerald-950/40",
description:
"Powered by Tailwind CSS — override any style without fighting specificity wars.",
icon: "🎨",
title: "Fully themeable",
},
{
bg: "bg-amber-50 dark:bg-amber-950/40",
description:
"Built on Base UI primitives so unstyled logic stays separate from visual styles.",
icon: "🧩",
title: "Headless foundation",
},
];
export default function Particle() {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false });
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!emblaApi) return;
const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
emblaApi.on("select", onSelect);
return () => {
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
return (
<div className="w-full max-w-sm space-y-4">
<div className="overflow-hidden rounded-xl" ref={emblaRef}>
<div className="flex">
{FEATURES.map((f, i) => (
<div className="min-w-0 shrink-0 grow-0 basis-full" key={i}>
<div className={cn("flex flex-col gap-4 rounded-xl p-6", f.bg)}>
<span className="text-3xl">{f.icon}</span>
<div>
<p className="font-semibold">{f.title}</p>
<p className="mt-1 text-muted-foreground text-sm leading-relaxed">
{f.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex items-center justify-between px-1">
<div className="flex gap-1.5">
{FEATURES.map((_, i) => (
<button
aria-label={`Go to ${i + 1}`}
className={cn(
"h-1.5 rounded-full transition-all",
i === current
? "w-6 bg-primary"
: "w-1.5 bg-muted-foreground/30",
)}
key={i}
onClick={() => emblaApi?.scrollTo(i)}
type="button"
/>
))}
</div>
<div className="flex gap-2">
<button
className="rounded-full border px-3 py-1 text-xs hover:bg-muted disabled:opacity-30"
disabled={current === 0}
onClick={() => emblaApi?.scrollPrev()}
type="button"
>
Prev
</button>
<button
className="rounded-full border bg-primary px-3 py-1 text-primary-foreground text-xs hover:bg-primary/90 disabled:opacity-30"
disabled={current === FEATURES.length - 1}
onClick={() => emblaApi?.scrollNext()}
type="button"
>
{current === FEATURES.length - 1 ? (
<span className="flex items-center gap-1">
<CheckIcon className="size-3" /> Done
</span>
) : (
"Next"
)}
</button>
</div>
</div>
</div>
);
}

