Skip to content

Installation

pnpm dlx shadcn@latest add @wisty/carousel

The CLI installs the Radix or Base UI file to match your project, with what it builds on: button.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • To show a few announcements, promotions or featured articles in one spot, one at a time, as on an Experience Cloud or app home page. It replaces lightning-carousel and lightning-carousel-image.
  • Each panel is an image with a short title and a line under it, and may link somewhere.
  • Three to five panels. People rarely look past the first few.

Don't use it

  • People need to compare the items or read them all. Show them side by side in a grid of Cards, or in a list with Item.
  • The panels are sections of one record or page. Use Tabs.
  • The content is wider than its box and people just need to scroll it. Use Scroll Area.
  • Anything people must not miss. A panel that isn't showing is easy to skip, so put it on the page or in an Alert.

Usage

Carousel
├── CarouselContent
│   └── CarouselItem
├── CarouselPrevious
└── CarouselNext
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"
<Carousel aria-label="Featured articles">
  <CarouselContent>
    <CarouselItem>{/* first panel */}</CarouselItem>
    <CarouselItem>{/* second panel */}</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

Dots and an autoplay button aren't parts: draw them beside the carousel from its API, as the examples do. setApi hands you the carousel's API once it's ready; read the shown panel with selectedScrollSnap() and listen for "select". Pass a function that keeps its identity, a state setter or one wrapped in useCallback, since the carousel calls it again whenever it changes.

const onApi = useCallback((api: CarouselApi) => {
  api?.on("select", () => setCurrent(api.selectedScrollSnap()))
}, [])

opts passes options to the carousel, like loop, and plugins adds behavior. Autoplay is the embla-carousel-autoplay plugin, which you add to the project yourself (pnpm add embla-carousel-autoplay).

Examples

Autoplay

Moves to the next panel every five seconds, with a button that stops and starts it. Under reduced motion it waits for the button.

Previous and next

Arrow buttons on each side, from `CarouselPrevious` and `CarouselNext`, and no dots.

API reference

PartBuilt on
Carousel<div> (opens in a new tab)
CarouselContent<div> (opens in a new tab)
CarouselItem<div> (opens in a new tab)
CarouselPreviousButton
CarouselNextButton
PropTypeDefault
optsCarouselOptions
pluginsCarouselPlugin
orientation"horizontal" | "vertical""horizontal"
setApi(api: CarouselApi) => void

CarouselPrevious

PropTypeDefault
variant"default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | "outline-brand" | "destructive-text" | "success" | "inverse""outline"
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""icon-sm"

CarouselNext

PropTypeDefault
variant"default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | "outline-brand" | "destructive-text" | "success" | "inverse""outline"
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""icon-sm"

useCarouselhook

Returns CarouselContextProps.

CarouselApitype

type CarouselApi = UseEmblaCarouselType[1]

CarouselOptionstype

type CarouselOptions = UseCarouselParameters[0]

CarouselPlugintype

type CarouselPlugin = UseCarouselParameters[1]

CarouselContextPropstype

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0]
  api: ReturnType<typeof useEmblaCarousel>[1]
  scrollPrev: () => void
  scrollNext: () => void
  canScrollPrev: boolean
  canScrollNext: boolean
} & CarouselProps

SLDS mapping

Carousel is slds-carousel and its slds-carousel__stage, CarouselContent the slds-carousel__panels that move, and CarouselItem an slds-carousel__panel. The examples draw the rest from the --carousel-* tokens: the bordered panel (slds-carousel__panel-action) with its image (slds-carousel__image) and text (slds-carousel__content, slds-carousel__content-title), the dots (slds-carousel__indicators, slds-carousel__indicator-action, with aria-selected doing the job of slds-is-active) and the autoplay button (slds-carousel__autoplay).

Known gaps:

  • No dots or autoplay button as parts. Copy them from the examples.
  • SLDS has no previous and next buttons; CarouselPrevious and CarouselNext are bordered icon buttons outside the panels.
  • SLDS moves panels sideways only, so orientation="vertical" has no SLDS look.
  • The autoplay button is 24px, where SLDS's is 20px (slds-button_icon-x-small).
PropValueSLDSNotes
orientationhorizontalslds-carouselSLDS moves its panels sideways only.
orientationverticalnoneKept from shadcn.

Keyboard interactions

Keys Action
Right Arrow, Left Arrow Anywhere in the carousel, shows the next or previous panel. They stop at the ends unless opts sets loop.
Tab Moves through the autoplay button, the shown panel's link and the selected dot, in that order.
Enter, Space On a dot, shows its panel. On the autoplay button, stops or starts it.

The arrows are Left and Right in a vertical carousel too. In the examples they also move focus to the newly selected dot, so focus never stays on a panel that has just been hidden.

Accessibility

  • Name the carousel with aria-label on Carousel. It's a region with the role description "carousel", and each CarouselItem a group described as "slide": give each one an aria-label like "2 of 3".
  • With dots, make them a tablist of tabs with aria-selected, aria-controls and a roving tabIndex, and each CarouselItem a tabpanel labelled by its dot, as the first example does. Set inert on the panels that aren't showing, so neither Tab nor a screen reader reaches them.
  • Give every dot an aria-label, like its panel's title: a dot has no text.
  • An autoplay button comes first in the carousel, before the panels, and says what it will do: "Stop autoplay" while playing, "Start autoplay" while stopped. Don't add aria-pressed to a label that changes.
  • Autoplay stops for good once someone drags a panel or focuses one, and stays stopped until they press the button.
  • When a panel's title says everything its image does, give the image alt="". Otherwise describe it.

Motion

  • Panels slide when they change, from the arrows, the dots, a drag or autoplay. Reduced motion doesn't change that slide.
  • The autoplay example doesn't start by itself under reduced motion, and once started from its button it jumps between panels instead of sliding (the plugin's breakpoints, with playOnInit: false and jump: true). Keep both if you change the plugin's options.

Responsive behavior

  • The carousel fills its container's width and each panel takes all of it. Set a max-w-* on Carousel.
  • CarouselPrevious and CarouselNext sit 3rem outside the panels, so leave that room on both sides or move them with className.

Rules

  • Don't autoplay without a button that stops it, and keep the reduced-motion options from the example.
  • Pass setApi a function that keeps its identity, or the carousel subscribes again on every render.
  • Keep CarouselItems as the direct children of CarouselContent. The carousel moves exactly those.
  • Draw panels and dots with the --carousel-* tokens, not literal colors or radii: they change per theme.