Components
Carousel and AspectRatio
A horizontal rail of items, built on the platform's own scroll-snap engine — so momentum, touch, trackpad gestures and RTL come for free — plus the two things it does not have: controls and a keyboard contract.
Examples
import { Carousel, CarouselControls, CarouselItem, CarouselTrack } from "@luc/ui";A real rail
The track is a labelled region with a tab stop, so a keyboard user can arrow-scroll it. Dots are a tablist, arrows disable at the ends, and the live region announces the current slide only when it changes.
<Carousel label="Motion primitives">
<CarouselTrack>{items}</CarouselTrack>
<CarouselControls />
</Carousel>Autoplay, when it is on
Autoplay is opt-in, pauses on hover and focus, stops while the tab is hidden, and never starts at all under prefers-reduced-motion.
<Carousel label="Release notes" autoplay interval={5000}>
<CarouselTrack gap="0.75rem" itemWidth="100%">
{releases.map((release) => (
<CarouselItem key={release.version}>
<Card variant="outline" padding="md">{release.text}</Card>
</CarouselItem>
))}
</CarouselTrack>
</Carousel>AspectRatio
Space is reserved before the media loads, so a card grid never shifts under a slow image. Use a preset, or pass any number.
{(["square", "video", "photo", "portrait"] as const).map((preset) => (
<div key={preset} className="flex flex-col gap-2">
<AspectRatio ratio={preset} className="rounded-lg border border-line-subtle">
<span className="absolute inset-0 grid place-items-center text-micro">{preset}</span>
</AspectRatio>
<Button size="sm" variant="ghost">Use preset</Button>
</div>
))}API
| Prop | Type | Default | Notes |
|---|---|---|---|
Carousel label | string | — | Required — a carousel is a named landmark. |
Carousel autoplay / interval | boolean · number | false · 6000 | Off by default; pauses on hover, focus and a hidden tab. |
CarouselTrack itemWidth / gap | string | "min(20rem, 82%)" · "1rem" | The rhythm lives on the track, so every item matches. |
CarouselItem label | string | — | Accessible name per slide; defaults to “Slide n of m”. |
CarouselControls dots / arrows | boolean | true | Dots are a tablist; arrows disable at the ends. |
AspectRatio ratio | number | preset | "square" | square · video · photo · wide · portrait · ultrawide, or any number. |
Usage
import { Carousel, CarouselTrack, CarouselControls, CarouselItem } from "@luc/ui";<Carousel label="Release notes">
<CarouselTrack>
<CarouselItem>…</CarouselItem>
</CarouselTrack>
<CarouselControls />
</Carousel>Guidelines
Do
- Use `CarouselControls` rather than custom buttons: they carry the names, the `aria-controls` tie and the position announcement.
- Keep `label` describing the set (“Release notes”), and write `itemLabel` as the position of each slide.
- Leave `autoplay` off when a slide holds text a reader needs; the interval only stops for a reader who pauses it.
Don't
- Put a long document in a carousel: a reader who navigates by heading should not have to page through it.
Accessibility
The rail is a region with `aria-roledescription="carousel"` and the carousel's own name. Controls are real buttons tied to the rail with `aria-controls`, and a polite live region announces the position as it changes.
Related