Carousel
A scroll-snap list with a previous and a next button. The track is an overflow container, so the browser does the dragging, flinging, snapping and mirroring, and each button is one call to scrollBy. No autoplay.
Example (a11y tier)
Drag or swipe the track, use the buttons, or Tab to the track and press the arrow keys. The second one loops and shows two slides at a time.
Import
import Carousel from 'abaabil/carousel' // structure only, no ARIAimport Carousel from 'abaabil/carousel/styled' // + CSSimport Carousel from 'abaabil/carousel/a11y' // + region, slide groups, live regionThe browser already has the physics
Embla, Swiper and react-slick each ship drag physics in JavaScript: pointer tracking, velocity, momentum, snapping, right-to-left. A scroll container has every one of those already. scroll-snap-type: x mandatory lands on a slide, touch and trackpad fling it with native momentum, and a right-to-left page mirrors it, arrows and all. There is no state to hold, because the scroll position is the state and the browser holds it.
So each button calls scrollBy on the track by one viewport width, signed by the container's text direction because scrollLeft runs negative in RTL. With loop, a step past either end is a scrollTo the other end instead. That is the whole of the lower two tiers, which is why they render in a Server Component tree with no client JavaScript.
Reduced motion, honoured twice
The stylesheet sets scroll-behavior: smooth on the track and drops it under prefers-reduced-motion. That covers a drag and the arrow keys, but not the buttons: the behavior argument to scrollBy overrides the stylesheet, so the button reads the media query again at click time and passes 'auto' when motion is reduced.
No autoplay
On purpose. An autoplaying carousel that is ever added here would have to pause on hover, on focus, and whenever reduced motion is requested, or it fails WCAG 2.2.2 for everyone who reads slower than the timer. Until someone needs it badly enough to carry that, it is not here.
One custom property: set --carousel-per-view: 3 on the carousel to show three slides at once. Each slide is calc(100% / var(--carousel-per-view, 1)) wide and snaps at its start, so a step still moves one viewport.
What the a11y tier adds
The platform scrolls the track; it does not say what the thing is or where you are in it. The a11y tier adds:
- A
regionnamed bylabel, witharia-roledescription="carousel". It warns in development without a name. - A
groupper slide, role description "slide", named "n of N". The track and slides are divs here rather than the normal tier's list, becauserole="group"is not permitted on an<li>. - The track in the tab order, so the arrow keys page it natively.
aria-controlsfrom each button to the track, andaria-disabled(notdisabled, so it stays in the tab order) on a button with nowhere to go when not looping.- A polite live region that says "Slide n of N" once a scroll settles: on the native
scrollendevent, or 150ms after the lastscrollevent where that does not exist, so a fling reports once, at rest.
Props
| Prop | Type | Default | Tier | Description |
|---|---|---|---|---|
items | ReactNode[] | — | all | One slide each. Or pass children, one slide per child. |
loop | boolean | false | all | Next at the last slide returns to the first; Previous at the first goes to the last. |
prevLabel | string | 'Previous' | all | Visually hidden; the arrow is drawn in CSS. |
nextLabel | string | 'Next' | all | Visually hidden. |
className | string | — | all | Merged with the base class. |
label | string | — | a11y | Names the region and the track. Warns in development without one. |