Carousel
Shows one image panel at a time, with dots or arrows to move between them.
Installation
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-carouselandlightning-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
└── CarouselNextimport {
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.
Downtown store
Open every day from 9 AM to 8 PM.
Harbor store
Open Monday to Saturday from 10 AM to 6 PM.
Airport store
Open every day from 6 AM to 10 PM, past security.
API reference
| Part | Built on |
|---|---|
Carousel | <div> (opens in a new tab) |
CarouselContent | <div> (opens in a new tab) |
CarouselItem | <div> (opens in a new tab) |
CarouselPrevious | Button |
CarouselNext | Button |
Carousel
| Prop | Type | Default |
|---|---|---|
opts | CarouselOptions | |
plugins | CarouselPlugin | |
orientation | "horizontal" | "vertical" | "horizontal" |
setApi | (api: CarouselApi) => void |
CarouselPrevious
| Prop | Type | Default |
|---|---|---|
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
| Prop | Type | Default |
|---|---|---|
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
} & CarouselPropsSLDS 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;
CarouselPreviousandCarouselNextare 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).
| Prop | Value | SLDS | Notes |
|---|---|---|---|
orientation | horizontal | slds-carousel | SLDS moves its panels sideways only. |
orientation | vertical | none | Kept 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-labelonCarousel. It's a region with the role description "carousel", and eachCarouselItema group described as "slide": give each one anaria-labellike "2 of 3". - With dots, make them a
tablistoftabs witharia-selected,aria-controlsand a rovingtabIndex, and eachCarouselItematabpanellabelled by its dot, as the first example does. Setinerton 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-pressedto 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, withplayOnInit: falseandjump: 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-*onCarousel. CarouselPreviousandCarouselNextsit 3rem outside the panels, so leave that room on both sides or move them withclassName.
Rules
- Don't autoplay without a button that stops it, and keep the reduced-motion options from the example.
- Pass
setApia function that keeps its identity, or the carousel subscribes again on every render. - Keep
CarouselItems as the direct children ofCarouselContent. The carousel moves exactly those. - Draw panels and dots with the
--carousel-*tokens, not literal colors or radii: they change per theme.