Carousel
Steps through slides one at a time. Swiping, keyboard navigation and RTL are all supported.
import { Carousel } from 'neba';
<Carousel label="Product highlights">
<img src="/one.jpg" alt="" />
<img src="/two.jpg" alt="" />
</Carousel>;Every top-level child becomes one slide. There is no slide sub-component: the snap point, the width and the role="group" / aria-roledescription="slide" pair are added for you.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| locale | string | — | BCP 47 tag: the region name, the arrows and every slide name |
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the frame, said the way a container says it: the sheet is never dyed, because a carousel holds other people’s pictures. `text` when they have edges of their own |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The frame’s radius, the arrows and how far they sit in, and the size of the dots |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| value | number | — | Which slide is showing, counted from 0 |
| defaultValue | number | 0 | Which starts showing |
| onValueChange | (index: number) => void | — | Called when the slide changes: including when it changed because somebody swiped |
| loop | boolean | true | Whether the arrows wrap from the last slide back to the first. With it off they go inert at the ends instead, which is the honest thing for a set that has a beginning and an end |
| autoPlay | boolean | false | Advances on its own. Off by default and deliberately: a carousel that moves while it is being read is the most complained-about pattern on the web. It pauses on hover, on focus anywhere inside it, and in a background tab, and does not start at all for a reader who asked for reduced motion. Turning it on draws a button that stops it, under the frame |
| interval | number | 5000 | How long each slide is held, in milliseconds |
| arrows | boolean | true | The previous/next buttons |
| indicators | boolean | true | The row of position dots under the frame |
| label | string | — | The accessible name. It has a default rather than being optional: a region with no name is a region nobody can skip |
| previousLabel | string | — | The previous button’s name |
| nextLabel | string | — | The next button’s name |
| pauseLabel · playLabel | string | — | The rotation control's two names: while the slides are advancing, and once they have been stopped. Default to the locale's wording |
| slideLabel | (index: number, count: number) => string | — | How one slide is named to a screen reader, and how its dot is labelled. Defaults to the locale's wording |
| children | ReactNode | — | The slides. Every top-level child becomes one: the wrapper carries the snap point, the width and the roles, none of which anybody should have to put on a photograph |
Every native <div> attribute passes through.
Underneath it is a scroll container with CSS scroll snapping. That is what makes swiping the browser's own behaviour, flips the direction automatically under RTL, and puts the transition on scroll-behavior: smooth. Which becomes an instant cut under prefers-reduced-motion through the same code path.
Examples
loop · arrows · indicators
Without loop, the arrows go inert at the ends, which suits a set that has a first and a last. arrows and indicators draw the side arrows and the dots beneath.
The arrows are drawn over the frame, so a slide with text near its edges should pad far enough in to clear them: about 3.5rem at size="md".
Photographs
A picture fills the frame, so there is nothing to pad in and the arrows sit over the image. Each slide is one Image with a ratio, which keeps the strip one height while the files load.
value and onValueChange
Controlled, the strip can be driven by something else on the page. onValueChange also fires when the slide changed because somebody swiped.
autoPlay and interval
autoPlay is off by default. With it on, it pauses on hover, on focus anywhere inside, and in a background tab, and it does not start at all under prefers-reduced-motion. The live region announcing the current slide stays silent while it runs, and starts again once the slides are stopped.
Turning it on also draws a button that stops the rotation, in the row under the frame beside the dots. It has no prop to remove it: hover and focus are not a mechanism for a reader on a phone or a magnifier. pauseLabel and playLabel name it.
If every slide has to be read, consider Tabs or a plain vertical stack instead.
Accessibility
labelbecomes the carousel's accessible name.previousLabel·nextLabel·pauseLabel·playLabel·slideLabelname the controls.autoPlaydraws its own stop button under the frame, so the rotation can be stopped by a reader who neither hovers nor tabs.- Each slide carries
role="group"andaria-roledescription="slide".
What is not offered
- More than one slide in view: use Grid with
overflow-x-auto. - Vertical: a scrolling list already does that.
- Fade: it cannot be combined with a scroll-based implementation.
localedecides the region name, the arrows and every slide name;labelandslideLabelwrite them out instead.