Carousel
A horizontally scrolling set of slides built on native CSS scroll-snap — so swipe, trackpad scrolling, and momentum all come from the browser.
Usage#
import {
Carousel,
CarouselContent,
CarouselDots,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "neelam-ui";
<Carousel aria-label="Featured items">
<CarouselContent>
<CarouselItem>One</CarouselItem>
<CarouselItem>Two</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
<CarouselDots />
</Carousel>CarouselDots reads the current slide from context, so it must live inside
Carousel — it throws otherwise.
Scroll-snap, not transforms#
The strip is a native scroll container with CSS scroll-snap rather than a
drag-and-transform implementation. Swipe physics, trackpad scrolling, and
momentum are the browser's, not a re-implementation — the same reasoning behind
Dialog using the native <dialog> element.
Which slide counts as current is tracked with an IntersectionObserver watching
each item against the strip, rather than computed from scrollLeft and an
assumed item width, so it stays correct even when slides are different sizes.
Showing more than one slide#
CarouselItem fills the carousel by default. Override its className with a
fractional basis to show several at once:
<CarouselItem className="basis-1/2">…</CarouselItem>
<CarouselItem className="basis-1/3">…</CarouselItem>Label it#
Carousel is role="region" with aria-roledescription="carousel". A region
needs an accessible name, so pass aria-label describing what the slides are —
"Featured items", "Customer quotes".
Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Reaches the scroll strip, then the previous, next, and dot controls. |
| ←→ | Moves to the previous or next slide when the carousel has focus. |
| EnterSpace | Activates the focused control. |
The scrolling viewport itself is focusable, which is what satisfies WCAG 2.1.1 for a region scrollable by pointer — the previous/next buttons are an affordance, not the only route.
Accessibility#
role="region"witharia-roledescription="carousel"; each slide isrole="group"witharia-roledescription="slide", so a screen reader announces where the user is.- Previous and next are icon-only buttons with explicit
aria-labels, and they disable themselves at the ends rather than silently doing nothing. - Dots carry
aria-label="Go to slide N"andaria-currenton the active one. - Nothing auto-advances. That is deliberate: motion the user did not start is a WCAG 2.2.2 problem, and a slide that moves out from under someone reading it is hostile.
Carousels hide content
Anything past the first slide is unseen by most people. If the content matters, consider a grid or a list instead — a carousel is a space-saving device, and the space it saves is paid for in attention.
API reference#
Carousel#
| Prop | Type | Default |
|---|---|---|
onSelectCalled with the new index whenever the current slide changes — from the nav buttons, `CarouselDots`, a swipe, or a scroll. | ((index: number) => void) | — |
CarouselItem, CarouselPrevious, CarouselNext, CarouselDots#
These add no props of their own — each is a styled wrapper around a native
element, and every attribute passes through. CarouselItem is a <div>; the
controls are <button>s with their labels applied for you.