abaabil

Carousel

35th lightest of the forty, by js weight

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 ARIA
import Carousel from 'abaabil/carousel/styled'  // + CSS
import Carousel from 'abaabil/carousel/a11y'    // + region, slide groups, live region

The 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 region named by label, with aria-roledescription="carousel". It warns in development without a name.
  • A group per slide, role description "slide", named "n of N". The track and slides are divs here rather than the normal tier's list, because role="group" is not permitted on an <li>.
  • The track in the tab order, so the arrow keys page it natively.
  • aria-controls from each button to the track, and aria-disabled (not disabled, 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 scrollend event, or 150ms after the last scroll event where that does not exist, so a fling reports once, at rest.

Props

PropTypeDefaultTierDescription
itemsReactNode[]—allOne slide each. Or pass children, one slide per child.
loopbooleanfalseallNext at the last slide returns to the first; Previous at the first goes to the last.
prevLabelstring'Previous'allVisually hidden; the arrow is drawn in CSS.
nextLabelstring'Next'allVisually hidden.
classNamestring—allMerged with the base class.
labelstring—a11yNames the region and the track. Warns in development without one.