GitHub

Carousel

A carousel with motion and swipe gestures built on Embla Carousel.

CMSPayload CMS Integration

Carousels can be populated from CMS image galleries with configurable autoplay, loop settings, and slide content managed by content editors.

CMS Demo

See how content editors configure carousels in a CMS:

Loading...
CMS ConfigurationSimulates Payload CMS carousel configuration with image galleries

Payload Block Example

// In your Payload CMS blocks config
export const CarouselBlock = {
  slug: "carousel",
  fields: [
    {
      name: "slides",
      type: "array",
      minRows: 1,
      fields: [
        {
          name: "image",
          type: "upload",
          relationTo: "media",
          required: true,
        },
        { name: "alt", type: "text", localized: true },
        { name: "caption", type: "text", localized: true },
      ],
    },
    {
      name: "options",
      type: "group",
      fields: [
        { name: "loop", type: "checkbox", defaultValue: false },
        { name: "autoplay", type: "checkbox", defaultValue: false },
        {
          name: "autoplayDelay",
          type: "number",
          defaultValue: 3000,
          admin: {
            condition: (data, siblingData) => siblingData?.autoplay,
          },
        },
      ],
    },
  ],
}

Rendering the Carousel

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
} from "@mordecai-design-system/ui"

export function ImageCarousel({ slides, options }) {
  return (
    <Carousel opts={{ loop: options.loop }}>
      <CarouselContent>
        {slides.map((slide, index) => (
          <CarouselItem key={index}>
            <img
              src={slide.image.url}
              alt={slide.alt}
              className="w-full rounded-lg"
            />
            {slide.caption && (
              <p className="mt-2 text-center text-muted-foreground">
                {slide.caption}
              </p>
            )}
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}

Basic Usage

Loading...
CarouselImage carousel with navigation

Installation

pnpm add @corew500/ui

Usage

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
} from "@mordecai-design-system/ui"
<Carousel>
  <CarouselContent>
    <CarouselItem>Slide 1</CarouselItem>
    <CarouselItem>Slide 2</CarouselItem>
    <CarouselItem>Slide 3</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

Examples

With Loop

<Carousel opts={{ loop: true }}>
  <CarouselContent>
    {items.map((item, index) => (
      <CarouselItem key={index}>{item}</CarouselItem>
    ))}
  </CarouselContent>
</Carousel>

Vertical Orientation

<Carousel orientation="vertical" className="h-[200px]">
  <CarouselContent>
    <CarouselItem>Item 1</CarouselItem>
    <CarouselItem>Item 2</CarouselItem>
  </CarouselContent>
</Carousel>

Multiple Items Per View

<Carousel>
  <CarouselContent className="-ml-2">
    {items.map((item, index) => (
      <CarouselItem key={index} className="basis-1/3 pl-2">
        {item}
      </CarouselItem>
    ))}
  </CarouselContent>
</Carousel>

Loading State

Show skeleton placeholders while slides are loading:

const { data, isLoading } = useQuery({ queryKey: ["slides"], queryFn: fetchSlides })

<Carousel loading={isLoading} loadingItems={3}>
  <CarouselContent>
    {data?.map((slide, index) => (
      <CarouselItem key={index}>
        <img src={slide.url} alt={slide.alt} />
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

| Prop | Type | Default | Description | |------|------|---------|-------------| | loading | boolean | false | Show skeleton loading state | | loadingItems | number | 3 | Number of skeleton slides to display | | loadingItemRenderer | (index) => ReactNode | - | Custom skeleton slide renderer |

Custom skeleton renderer:

<Carousel
  loading
  loadingItems={4}
  loadingItemRenderer={(index) => (
    <div className="space-y-2">
      <Skeleton className="h-[300px] w-full rounded-xl" />
      <Skeleton className="h-4 w-1/2" />
    </div>
  )}
>
  {/* Content */}
</Carousel>

Sub-components

| Component | Description | |-----------|-------------| | Carousel | Root container with context | | CarouselContent | Scrollable container for items | | CarouselItem | Individual slide | | CarouselPrevious | Previous slide button | | CarouselNext | Next slide button |

Props

Carousel

PropTypeDefaultDescription
optsPartial<OptionsType>——
pluginsCreatePluginType<LoosePluginType, {}>[]——
orientationenumhorizontal—
setApi(api: EmblaCarouselType) => void——
loadingenumfalseShow skeleton loading state
loadingItemsnumber3Number of skeleton items to show when loading (default: 3)
loadingItemRenderer(index: number) => ReactNode—Custom skeleton item renderer

CarouselItem

CarouselItem accepts all standard div element props. Use className for sizing (e.g., basis-1/3 for multiple items per view).

Hooks

useCarousel

Access carousel context from child components:

function SlideCounter() {
  const { api, canScrollPrev, canScrollNext } = useCarousel()
  // Use carousel state
}