Carousel

free

Carousel presents ordered media or panels one at a time inside the current surface.

Preview
Dark
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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 Carousel

Examples

Basic

Preview
Dark
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 1
Gallery image 2
Gallery image 3
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>
  );
}
Preview
Dark
Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
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.

PropDescriptionTypeDefault
childrenCarousel content and controls, usually Carousel.Content, Carousel.Item, Carousel.Previous, and Carousel.Next.ReactNode-
classNameClass names applied to the carousel root.string-
orientationDirection used for keyboard navigation, drag movement, and item spacing.CarouselOrientation'horizontal'
optsCarousel behavior options. The current implementation applies loop and startIndex.CarouselOptions{}
setApiReceives the imperative carousel API for external controls and slide state.CarouselApiHandler-
refRef forwarded to the carousel root.Ref<HTMLDivElement>-
...nativeDivPropsNative props for the carousel root div.NativeCarouselDivProps-
PropDescriptionTypeDefault
childrenCarousel items rendered inside the draggable content strip.ReactNode-
classNameClass names applied to the moving content strip.string-
refRef forwarded to the content strip.Ref<HTMLDivElement>-
...nativeDivPropsNative props for the content strip div.NativeCarouselContentProps-
PropDescriptionTypeDefault
childrenSlide content.ReactNode-
classNameClass names applied to the slide wrapper. Use basis utilities for multi-slide layouts.string-
refRef forwarded to the slide wrapper.Ref<HTMLDivElement>-
...nativeDivPropsNative props for the slide wrapper div.NativeCarouselItemProps-
PropDescriptionTypeDefault
classNameClass names applied to the trigger button for placement or local layout.string-
variantButton variant used by the trigger.ButtonVariant'default'
sizeButton size used by the trigger.ControlSize'sm'
disabledOverride the trigger disabled state. If omitted, the trigger is disabled when it cannot scroll.booleanDerived from scroll state
...buttonPropsButton 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'
>;