Components
Carousel
A swipeable, keyboard-navigable slider built on Embla for stepping through cards, media, or any horizontal collection of content.
Basic usage
Compose CarouselContent with one CarouselItem per slide, then add the previous and next controls. Both controls carry a visually hidden label — “Previous slide” and “Next slide” — that follows the language of the nearest ComitorLocaleProvider; srLabel overrides it on one control.
Multiple per view
Set a fractional basis on each item (e.g. basis-1/3) and align slides to the start to show several at once.
Infinite loop
Pass opts={{ loop: true }} so the carousel wraps around and the controls never disable at the ends.
Custom controls with useCarousel
The package exports the useCarousel() hook so an app can build its own indicators or controls without re-wiring Embla. Called from any component rendered inside Carousel, it returns the live api, scrollPrev/scrollNext, and the canScrollPrev/canScrollNext flags; outside the tree, use setApi instead. Give indicator dots the --primary-ink and --control-edge tokens so the active one is legible on both palettes.
Vertical
orientation="vertical" stacks the track and moves the controls above and below it. Give CarouselContent a fixed height — vertical Embla has nothing to measure otherwise. The arrow keys stay ← and → in both orientations.
Props
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Scroll axis. Sets the Embla axis and switches the item spacing from pl-4 to pt-4; the controls rotate 90° and move above and below the track. |
opts | CarouselOptions | — | Embla options forwarded to the engine, e.g. { align: "start", loop: true }. axis is always taken from orientation. |
plugins | CarouselPlugin | — | Embla plugins such as Autoplay or WheelGestures. |
setApi | (api: CarouselApi) => void | — | Hands the Embla API to a component outside the carousel tree. Inside it, prefer useCarousel(). |
className | string | — | Merged onto the root region, which is position: relative so the controls can be absolutely placed. |
...rest | React.ComponentProps<'div'> | — | All native div props are forwarded to the region, which carries data-slot="carousel". |
CarouselPrevious & CarouselNext
| Prop | Type | Default | Description |
|---|---|---|---|
srLabel | string | 'Previous slide' / 'Next slide' | Visually hidden label on the button. Follows ComitorLocaleProvider (“Trang chiếu trước” / “Trang chiếu sau” under locale="vi"); a value passed here wins. |
variant | ButtonProps["variant"] | 'outline' | Any Button variant. The control keeps its size-8 rounded-full override on top. |
size | ButtonProps["size"] | 'icon' | Any Button size, though the control pins itself to size-8 via className. |
disabled | boolean | — | Set for you from canScrollPrev / canScrollNext; pass it only to force the button off. |
className | string | — | Merged onto the button — use it to reposition the control, e.g. inside the frame instead of outside it. |
...rest | ButtonProps | — | All remaining Button props are forwarded. Do not pass asChild: the control always renders an arrow plus an sr-only label, and Radix Slot takes exactly one child, so it throws. leftIcon and loading add a second icon or a spinner before the arrow. |
CarouselContent and CarouselItem take native div props only; set the per-slide width on the item with a basis-* class.
Accessibility
- The root is a
role="region"and each item arole="group". Theiraria-roledescriptiondefaults are “carousel” and “slide”, from the provider's carousel strings — “băng chuyền” and “trang chiếu” underlocale="vi". Both components spread the rest of their props last, so anaria-roledescriptionyou pass still wins. Give the root anaria-labelas well, so the region has a name and not only a role description. - Key handling sits on the region as
onKeyDownCapture, so ← and → step between slides while focus is anywhere inside the carousel — including on the controls. - The previous and next buttons are native
<button>elements with a focus-visible ring and a visually hidden label, which follows the provider's language;srLabelreplaces it when one control needs different words. - Controls become
disabledat the start and end of the track (unlessloopis enabled), removing them from the tab order when there is nowhere left to scroll. - Custom indicators built on
useCarouselneed their own names — a dot with noaria-labeland noaria-currentreads as an unlabelled button.