Carousel
A headless, scroll-snap carousel (Embla-style). It tracks the active slide from scroll position and exposes Previous/Next buttons, indicators, keyboard arrows, and optional looping.
Features
Native scroll-snap — tracks the active slide from scroll position.
Horizontal or vertical orientation.
Previous/Next buttons auto-disable at the ends (unless looping).
Render-prop indicators reflect the selected slide.
Keyboard arrow navigation on the focused viewport.
Optional looping wraps from the last slide to the first.
Example
Anatomy
import { Carousel } from '@wire-ui/react'
<Carousel.Root>
<Carousel.Viewport>
<Carousel.Content>
<Carousel.Slide>Slide 1</Carousel.Slide>
<Carousel.Slide>Slide 2</Carousel.Slide>
<Carousel.Slide>Slide 3</Carousel.Slide>
</Carousel.Content>
</Carousel.Viewport>
<Carousel.Previous>‹</Carousel.Previous>
<Carousel.Next>›</Carousel.Next>
<Carousel.Indicators>
{({ index, selected, scrollTo }) => (
<button key={index} onClick={scrollTo} aria-current={selected} />
)}
</Carousel.Indicators>
</Carousel.Root>Styling
Carousel is headless. The Root sets data-orientation and the viewport applies the native scroll-snap behavior inline. Slides snap to the start edge. Previous/Next become disabled automatically when they cannot scroll further (use disabled: variants to style that state).
<Carousel.Previous className="
border border-black
disabled:opacity-30 disabled:cursor-not-allowed
">
‹
</Carousel.Previous>Using data attributes
Carousel.Root sets data-orientation; the internal elements are tagged for targeting.
/* Stack a vertical carousel */
[data-orientation="vertical"] [data-carousel-content] { flex-direction: column; }
/* Target the scroll container */
[data-carousel-viewport] { scrollbar-width: none; }Root props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Scroll axis |
loop | boolean | false | Wrap from last → first slide |
defaultIndex | number | 0 | Initial slide index |
onIndexChange | (index: number) => void | — | Called when the selected slide changes |
Root also accepts all native div attributes (except onChange).
Viewport props
| Prop | Type | Description |
|---|---|---|
...React.HTMLAttributes<HTMLDivElement> | — | The scroll container. Add tabIndex={0} to enable arrow-key navigation. |
Content props
| Prop | Type | Description |
|---|---|---|
...React.HTMLAttributes<HTMLDivElement> | — | The flex track that holds the slides |
Slide props
| Prop | Type | Description |
|---|---|---|
...React.HTMLAttributes<HTMLDivElement> | — | A single snap target; auto-registers with Root |
Previous / Next props
| Prop | Type | Description |
|---|---|---|
disabled | boolean | Override the automatic disabled state |
...React.ButtonHTMLAttributes<HTMLButtonElement> | — | Standard button attributes |
Indicators props
| Prop | Type | Description |
|---|---|---|
children | (props: { index: number; selected: boolean; scrollTo: () => void }) => ReactNode | Render function called once per slide |
Data attributes
| Attribute | Element | Values |
|---|---|---|
data-orientation | Root | "horizontal" / "vertical" |
data-carousel-viewport | Viewport | Present (marker) |
data-carousel-content | Content | Present (marker) |
data-carousel-slide | Slide | Present (marker) |
Accessibility
- Root sets
role="region"andaria-roledescription="carousel". - Each Slide sets
role="group"andaria-roledescription="slide". - Previous/Next render as
<button>witharia-label="Previous slide"/"Next slide"and are disabled at the ends (unlessloop).
Keyboard Interactions
Arrow keys work when the focused element is the Viewport (give it tabIndex={0}).
| Key | Description |
|---|---|
| ArrowRight / ArrowDown | Scroll to the next slide (axis depends on orientation). |
| ArrowLeft / ArrowUp | Scroll to the previous slide. |
| Tab | Moves focus to the viewport / controls. |
Last updated on