Carousel
A carousel with motion and swipe built using a tiny island.
Carousel is a horizontal or vertical slider. Each CarouselItem is a slide that snaps on scroll. Previous/Next buttons and the arrow keys handle navigation, alongside native scrolling (drag/wheel) and touch swipe.
Installation
$ bext ui add carousel
Copy and paste the following into src/components/ui/carousel.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="carousel" hooks.
Usage
Anatomy
<Carousel> ├── <CarouselContent> │ ├── <CarouselItem>slide 1</CarouselItem> │ ├── <CarouselItem>slide 2</CarouselItem> │ └── <CarouselItem>slide 3</CarouselItem> ├── <CarouselPrevious /> └── <CarouselNext />
API Reference
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | Scroll axis. Written to data-orientation; the buttons scroll by one viewport on that axis. |
| ariaLabel | string | — | Accessible name of the carousel region (aria-label on the root). |
| loop | boolean | false | Wraps from the last slide to the first and back. Without it, Previous and Next are disabled at the ends. Written to data-loop. |
| children | ReactNode | — | A CarouselContent, then CarouselPrevious and CarouselNext. |
| className | string | — | Extra classes on the root (relative). |
CarouselContent
| Prop | Type | Default | Description |
|---|---|---|---|
| ariaLabel | string | — | Names the scrollable track: when set, the track gets role="group" and this aria-label. The track is focusable, so set it. |
| children | ReactNode | — | The CarouselItems. |
CarouselItem
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | One slide. basis-full by default. To show several slides at once: basis-1/2 md:basis-1/3 … |
CarouselPrevious / CarouselNext
| Prop | Type | Default | Description |
|---|---|---|---|
| ariaLabel | string | "Previous slide" / "Next slide" | Accessible name of the button. |
| className | string | — | Extra classes on the round 32px button, for example to move it outside the track. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="carousel" | Carousel root | — | Scope used by the island to find the track from a button. |
| data-orientation | Carousel root | "horizontal" | "vertical" | Reflects the orientation prop. |
| data-loop | Carousel root | "true" (absent otherwise) | Reflects the loop prop. |
| data-ui-carousel-content | track inside CarouselContent | — | The scroll-snap container that the buttons scroll. |
| data-ui-carousel-prev / data-ui-carousel-next | CarouselPrevious / CarouselNext | — | Clicking scrolls the track by one viewport, backward or forward. The island sets the disabled attribute at the first and last position unless the carousel loops. |
| data-slot | every sub-component | "carousel" | "carousel-content" | "carousel-item" | "carousel-previous" | "carousel-next" | Stable selector for custom styling. |
Keyboard
| Key | Action |
|---|---|
| Tab | Focuses the track, then the Previous and Next buttons. |
| Arrow Left / Right | With the focus on the track or on a button of a horizontal carousel: scrolls by one viewport, backward or forward. |
| Arrow Up / Down | Same, for a vertical carousel. |
| Enter / Space | On Previous / Next: scrolls by one viewport. |
Accessibility
The root sets role="region" and aria-roledescription="carousel"; name it with ariaLabel. Each CarouselItem sets role="group" and aria-roledescription="slide"; it has no "N of M" label. The track is focusable (tabindex="0") and gets role="group" plus an aria-label only when you pass ariaLabel to CarouselContent.
CarouselPrevious and CarouselNext are named "Previous slide" and "Next slide" (ariaLabel to change them). Unless the carousel has loop, the island disables the button that has nowhere to go; if that button had the focus, the focus moves to the track so the arrow keys keep working. Slides that are scrolled out of view stay in the accessibility tree.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page