bx

Carousel

A carousel with motion and swipe built using a tiny island.

1
2
3
4
5
tsx
<div className="w-full max-w-md">
  <Carousel ariaLabel="Sample slides">
    <CarouselContent ariaLabel="Slides">
      {[1, 2, 3, 4, 5].map((n) => (
        <CarouselItem key={n}>
          <div className="flex aspect-square items-center justify-center rounded-md border bg-muted text-4xl font-semibold">{n}</div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
</div>

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

Terminal
$ bext ui add carousel

Usage

tsx
import { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } from "@bext-stack/ui/primitives"

<Carousel ariaLabel="Featured products">
  <CarouselContent ariaLabel="Slides">
    <CarouselItem>Slide 1</CarouselItem>
    <CarouselItem>Slide 2</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

Anatomy

<Carousel>
  ├── <CarouselContent>
  │   ├── <CarouselItem>slide 1</CarouselItem>
  │   ├── <CarouselItem>slide 2</CarouselItem>
  │   └── <CarouselItem>slide 3</CarouselItem>
  ├── <CarouselPrevious />
  └── <CarouselNext />

API Reference

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.

Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.