Carousel
A horizontal or vertical slider on Embla, with the arrows wired up. Embla's whole options object is exposed, so loop, alignment, and multi-slide scrolling are configured rather than reimplemented.
Import
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from "@mikenotthepope/substrateui"Composition
The two buttons are positioned outside the track — 12 units past its inline edges — so the carousel needs horizontal room around it or they will be clipped. Both are optional: drop them and the carousel is drag-and-swipe only.
Carousel
├── CarouselContent
│ └── CarouselItem
├── CarouselPrevious
└── CarouselNextMore than one slide at a time
An item is basis-full by default. Override the basis and several slides share the track — the pattern for a shelf of cards rather than a hero. opts goes to Embla, so align and loop come from there.
Vertical
orientation="vertical"switches Embla's axis, rotates the buttons to the top and bottom, and swaps the item's leading padding for top padding. The track needs a height.
Reading the position
setApi hands over Embla's api object. Subscribe to select for a slide counter or dot indicators, and unsubscribe on cleanup — the effect below is the whole pattern.
Slide 1 of 5
Direction
The buttons are positioned with -start-12 and -end-12, and the item's gap with ps-4, so the furniture lands on the correct sides in RTL without changes.
The arrows are ArrowLeft and ArrowRight glyphs, and they do notmirror themselves — in RTL the "previous" button sits on the right while still drawing a left-pointing arrow. Embla also needs telling which way the content runs. Pass opts={{ direction: "rtl" }} in an RTL locale and mirror the glyphs with className="rtl:-scale-x-100" on the two buttons. See Direction (RTL).
Labels
The two buttons contain an arrow and an sr-only name, so their accessible names come entirely from CarouselLabels. Override per instance with labels, or app-wide through LabelsProvider's carousel key — the provider is how you translate them once rather than at every call site.
| Prop | Type | Default | Description |
|---|---|---|---|
previousSlide | string | "Previous slide" | Screen-reader name for the previous button, whose visible content is an arrow. |
nextSlide | string | "Next slide" | Screen-reader name for the next button. |
Accessibility
The track is a region with aria-roledescription="carousel", and each item a group with aria-roledescription="slide". Arrow keys move between slides once the region has focus, and both buttons disable themselves at the ends unless loop is set.
Slides that scroll out of view stay in the DOM and in the tab order. If a slide holds a link or a button, a keyboard user will land on a control they cannot see — which is the strongest argument for not putting essential content in a carousel at all.
If you add autoplay through plugins, it must stop on hover and on focus, and there must be a way to stop it permanently. Motion that cannot be paused fails WCAG 2.2.2, and content that moves on its own is unusable for anyone reading slowly.
Announce position changes yourself, as the counter above does with aria-live="polite". Without it, pressing "next" is silent.
API Reference
opts and pluginsare Embla's own types passed through untouched; see the Embla options reference for that surface. CarouselContent and CarouselItem take plain div props; the two buttons take Button's, so variant and size work on them.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Which axis the slides run along. Sets Embla's axis and moves the buttons to the ends of that axis. |
opts | CarouselOptions | — | Passed straight to Embla — loop, align, slidesToScroll, dragFree, and the rest. |
plugins | CarouselPlugin | — | Embla plugins, such as autoplay or wheel gestures. |
setApi | (api: CarouselApi) => void | — | Hands you Embla's api object, for reading the selected index or scrolling programmatically. |
labels | CarouselLabels | — | Text overrides for the two navigation buttons: previousSlide, nextSlide. Set on CarouselPrevious / CarouselNext. |