Carousel
A scrolling track of slides with controls.
Usage
Built on CSS scroll-snap, not transforms. Touch swiping, trackpad flicks, momentum and the browser's own arrow-key scrolling all come from the platform — there is no gesture code here, and no new dependency.
label is required: the root is a labelled region, so a screen reader user
can find the whole carousel and skip past it in one move. Each slide is announced as "slide, 3 of
5" unless you give it a better name.
More than one at a time
Set --sve-carousel-slide-size on the root. The controls are disabled from
the measured scroll extent rather than from the slide index, which matters here: with three slides
visible the last index is reachable while there is still track left to scroll, and a control disabled
then would be lying.
Vertical
The arrow glyphs and the snap axis follow orientation.
Why it does not loop
An infinite track makes "slide 3 of 5" meaningless — there is no third of five on a ring — and
it removes the only honest position affordance a carousel has: a Previous or Next that goes flat at the boundary.
A sighted user loses a scrollbar; everyone else loses the ability to know they have reached the
end.
Position is also read from the scroll container on every scroll rather than remembered, because a swipe, a flick and the arrow keys are all drivers this component cannot intercept. A stored index would disagree with the screen after any of them, and the slide label would start lying.
Why it does not auto-rotate
Auto-rotation is only acceptable with all of: a visible pause control, stopping on hover,
stopping on focus, and honouring prefers-reduced-motion. That chain is
exactly what gets half-built, and content that moves on its own is one of the most documented
accessibility failures on the web — it steals attention from the rest of the page and it moves
the target out from under anyone who reads or clicks slowly.
So it is not built in. useCarousel() exposes next(), prev() and goTo(), so a consumer who genuinely needs rotation can drive it — and
owns the pause button that has to come with it.
The controls above are the whole API. Rotation is yours to justify.
Props
Each part also takes its native element's attributes via prop spreading.
| Prop | Type | Default |
|---|---|---|
orientation | Orientation | 'horizontal' |
label The carousel's accessible name, e.g. "Product photos". Required, and it is not decoration. The root is a labelled region so a screen reader user can find and skip the whole thing in one move; an unnamed region is an obstacle they have to read through to identify. | string | — |
labelledby Id of an existing visible heading, as an alternative to `label`. | string | — |
class Extra classes merged onto the root element. | string | — |
children required | Snippet | — |
| Prop | Type | Default |
|---|---|---|
label Overrides the generated "N of M" name. Pass something meaningful when the slide has an identity of its own — a product name beats a position. The default exists because a slide with no name at all is announced as "group", which tells the user nothing about where they are. | string | — |
class Extra classes merged onto the slide. | string | — |
children required | Snippet | — |
| Prop | Type | Default |
|---|---|---|
label Accessible name. Defaults to "Previous slide". | string | 'Previous slide' |
class Extra classes merged onto the button. | string | — |
children | Snippet | — |
| Prop | Type | Default |
|---|---|---|
index required Zero-based index of the slide this dot scrolls to. | number | — |
label Accessible name. Defaults to "Go to slide N". | string | — |
class Extra classes merged onto the button. | string | — |