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.

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.
optsCarouselOptions—Embla options forwarded to the engine, e.g. { align: "start", loop: true }. axis is always taken from orientation.
pluginsCarouselPlugin—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().
classNamestring—Merged 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'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.
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.
disabledboolean—Set for you from canScrollPrev / canScrollNext; pass it only to force the button off.
classNamestring—Merged onto the button — use it to reposition the control, e.g. inside the frame instead of outside it.
...restButtonProps—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 a role="group". Their aria-roledescription defaults are “carousel” and “slide”, from the provider's carousel strings — “băng chuyền” and “trang chiếu” under locale="vi". Both components spread the rest of their props last, so an aria-roledescription you pass still wins. 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, which follows the provider's language; srLabel replaces it when one control needs different words.
  • 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.