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.

1
2
3
4
5

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.

1
2
3
4
5
6
7
8

Infinite loop

Pass opts={{ loop: true }} so the carousel wraps around and the controls never disable at the ends.

Strategy
Audit
Compliance
Advisory

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.

Strategy
Audit
Compliance
Advisory

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.

Strategy
Audit
Compliance

Props

Carousel

PropTypeDefaultDescription
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.
optsCarouselOptionsEmbla options forwarded to the engine, e.g. { align: "start", loop: true }. axis is always taken from orientation.
pluginsCarouselPluginEmbla plugins such as Autoplay or WheelGestures.
setApi(api: CarouselApi) => voidHands the Embla API to a component outside the carousel tree. Inside it, prefer useCarousel().
classNamestringMerged onto the root region, which is position: relative so the controls can be absolutely placed.
...restReact.ComponentProps<'div'>All native div props are forwarded to the region, which carries data-slot="carousel".

CarouselPrevious & CarouselNext

PropTypeDefaultDescription
srLabelstring'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.
variantButtonProps["variant"]'outline'Any Button variant. The control keeps its size-8 rounded-full override on top.
sizeButtonProps["size"]'icon'Any Button size, though the control pins itself to size-8 via className.
disabledbooleanSet for you from canScrollPrev / canScrollNext; pass it only to force the button off.
classNamestringMerged onto the button — use it to reposition the control, e.g. inside the frame instead of outside it.
...restButtonPropsAll 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 a role="group". Their aria-roledescription defaults are the Vietnamese “băng chuyền” and “trang chiếu”; both components spread the rest of their props last, so passing aria-roledescription="carousel" and aria-roledescription="slide" overrides them. Give the root an aria-label as 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 set srLabel to match the language of the surrounding interface.
  • Controls become disabled at the start and end of the track (unless loop is enabled), removing them from the tab order when there is nowhere left to scroll.
  • Custom indicators built on useCarousel need their own names — a dot with no aria-label and no aria-current reads as an unlabelled button.