Carousel
freeCarousel presents ordered media or panels one at a time inside the current surface.
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function UsageDemo() {
return (
<div className="w-96 max-w-full">
<Carousel className="space-y-4">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="aspect-square w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<div className="flex justify-center gap-2">
<Carousel.Previous />
<Carousel.Next />
</div>
</Carousel>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add CarouselExamples
Basic
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function BasicDemo() {
return (
<div className="w-80 max-w-full">
<Carousel className="space-y-4">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="aspect-square w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<div className="flex justify-center gap-2">
<Carousel.Previous />
<Carousel.Next />
</div>
</Carousel>
</div>
);
}Sizes
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function SizesDemo() {
return (
<div className="w-full max-w-md">
<Carousel className="space-y-4">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item
key={image.src}
className="basis-1/2 sm:basis-1/3"
>
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="aspect-square w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<div className="flex justify-center gap-2">
<Carousel.Previous />
<Carousel.Next />
</div>
</Carousel>
</div>
);
}Loop
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function LoopDemo() {
return (
<div className="w-80 max-w-full">
<Carousel opts={{ loop: true }} className="space-y-4">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="aspect-square w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<div className="flex justify-center gap-2">
<Carousel.Previous />
<Carousel.Next />
</div>
</Carousel>
</div>
);
}Vertical
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function VerticalDemo() {
return (
<div className="w-80 max-w-full">
<Carousel orientation="vertical" className="relative py-12">
<Carousel.Content className="h-64">
{images.map((image) => (
<Carousel.Item key={image.src} className="basis-full">
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="h-56 w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<Carousel.Previous className="absolute left-1/2 top-0 -translate-x-1/2" />
<Carousel.Next className="absolute bottom-0 left-1/2 -translate-x-1/2" />
</Carousel>
</div>
);
}Trigger Placement
Preview
Dark
import Carousel from '@/components/ui/Carousel';
const images = Array.from({ length: 3 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
function ImageSlide({ src, alt }: { src: string; alt: string }) {
return (
<div className="overflow-hidden">
<img src={src} alt={alt} className="aspect-square w-full object-cover" />
</div>
);
}
export default function TriggerPlacementDemo() {
return (
<div className="grid w-full max-w-md grid-cols-1 gap-8 sm:grid-cols-2">
<Carousel className="relative">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<ImageSlide src={image.src} alt={image.alt} />
</Carousel.Item>
))}
</Carousel.Content>
<Carousel.Previous className="absolute left-2 top-1/2 z-10 -translate-y-1/2" />
<Carousel.Next className="absolute right-2 top-1/2 z-10 -translate-y-1/2" />
</Carousel>
<Carousel className="space-y-4">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<ImageSlide src={image.src} alt={image.alt} />
</Carousel.Item>
))}
</Carousel.Content>
<div className="flex justify-center gap-2">
<Carousel.Previous />
<Carousel.Next />
</div>
</Carousel>
</div>
);
}Gallery API
Preview
Dark
import { useState } from 'react';
import Carousel from '@/components/ui/Carousel';
import type { CarouselApi } from '@/components/ui/Carousel';
const images = Array.from({ length: 5 }, (_, index) => ({
src: `/img/thumbs/misc/img-${index + 1}.png`,
alt: `Gallery image ${index + 1}`,
}));
export default function GalleryApiDemo() {
const [api, setApi] = useState<CarouselApi | null>(null);
const current = api?.selectedIndex ?? 0;
return (
<div className="w-full max-w-md">
<Carousel setApi={setApi} className="space-y-4">
<div className="relative">
<Carousel.Content>
{images.map((image) => (
<Carousel.Item key={image.src}>
<div className="overflow-hidden">
<img
src={image.src}
alt={image.alt}
className="aspect-square w-full object-cover"
/>
</div>
</Carousel.Item>
))}
</Carousel.Content>
<Carousel.Previous className="absolute left-4 top-1/2 z-10 -translate-y-1/2" />
<Carousel.Next className="absolute right-4 top-1/2 z-10 -translate-y-1/2" />
</div>
<div className="flex justify-center gap-2">
{images.map((image, index) => (
<button
key={image.src}
type="button"
aria-label={`Go to image ${index + 1}`}
aria-current={current === index}
className={
current === index
? 'h-2 w-2 rounded-full bg-primary'
: 'h-2 w-2 rounded-full bg-muted'
}
onClick={() => api?.scrollTo(index)}
/>
))}
</div>
<div className="grid grid-cols-5 gap-2">
{images.map((image, index) => (
<button
key={image.src}
type="button"
aria-label={`Select image ${index + 1}`}
aria-current={current === index}
className={
current === index
? 'overflow-hidden rounded-control border border-primary ring-2 ring-primary/20'
: 'overflow-hidden rounded-control border hover:border-primary'
}
onClick={() => api?.scrollTo(index)}
>
<img
src={image.src}
alt=""
className="aspect-square w-full object-cover"
/>
</button>
))}
</div>
</Carousel>
</div>
);
}API
Carousel exposes a compound root with content, item, previous, and next subcomponents. The root accepts native div props.
Carousel
| Prop | Description | Type | Default |
|---|---|---|---|
children | Carousel content and controls, usually Carousel.Content, Carousel.Item, Carousel.Previous, and Carousel.Next. | ReactNode | - |
className | Class names applied to the carousel root. | string | - |
orientation | Direction used for keyboard navigation, drag movement, and item spacing. | CarouselOrientation | 'horizontal' |
opts | Carousel behavior options. The current implementation applies loop and startIndex. | CarouselOptions | {} |
setApi | Receives the imperative carousel API for external controls and slide state. | CarouselApiHandler | - |
ref | Ref forwarded to the carousel root. | Ref<HTMLDivElement> | - |
...nativeDivProps | Native props for the carousel root div. | NativeCarouselDivProps | - |
Carousel.Content
| Prop | Description | Type | Default |
|---|---|---|---|
children | Carousel items rendered inside the draggable content strip. | ReactNode | - |
className | Class names applied to the moving content strip. | string | - |
ref | Ref forwarded to the content strip. | Ref<HTMLDivElement> | - |
...nativeDivProps | Native props for the content strip div. | NativeCarouselContentProps | - |
Carousel.Item
| Prop | Description | Type | Default |
|---|---|---|---|
children | Slide content. | ReactNode | - |
className | Class names applied to the slide wrapper. Use basis utilities for multi-slide layouts. | string | - |
ref | Ref forwarded to the slide wrapper. | Ref<HTMLDivElement> | - |
...nativeDivProps | Native props for the slide wrapper div. | NativeCarouselItemProps | - |
Carousel.Previous And Carousel.Next
| Prop | Description | Type | Default |
|---|---|---|---|
className | Class names applied to the trigger button for placement or local layout. | string | - |
variant | Button variant used by the trigger. | ButtonVariant | 'default' |
size | Button size used by the trigger. | ControlSize | 'sm' |
disabled | Override the trigger disabled state. If omitted, the trigger is disabled when it cannot scroll. | boolean | Derived from scroll state |
...buttonProps | Button props except icon, shape, and aria-label, which the carousel trigger owns. | CarouselTriggerProps | - |
Types
type CarouselOrientation = 'horizontal' | 'vertical';
type CarouselOptions = {
align?: 'start' | 'center' | 'end';
loop?: boolean;
dragFree?: boolean;
skipSnaps?: boolean;
startIndex?: number;
};
type CarouselApi = {
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
selectedIndex: number;
scrollTo: (index: number) => void;
scrollSnapCount: number;
};
type CarouselApiHandler = (api: CarouselApi) => void;
type ButtonVariant = 'solid' | 'subtle' | 'default' | 'ghost' | 'link';
type ControlSize = 'sm' | 'md' | 'lg';
type NativeCarouselDivProps = ComponentProps<'div'>;
type NativeCarouselContentProps = ComponentProps<'div'>;
type NativeCarouselItemProps = ComponentProps<'div'>;
type CarouselTriggerProps = Omit<
ComponentPropsWithoutRef<typeof Button>,
'icon' | 'shape' | 'aria-label'
>;