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 that defaults to Vietnamese, so pass srLabel in an English interface.
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 | 'Trang chiếu trước' / 'Trang chiếu sau' | Visually hidden label on the button. The package default is Vietnamese — pass an English string in an English UI. |
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 the Vietnamese “băng chuyền” and “trang chiếu”; both components spread the rest of their props last, so passingaria-roledescription="carousel"andaria-roledescription="slide"overrides them. 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. That label defaults to Vietnamese, so setsrLabelto match the language of the surrounding interface. - 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.