# Carousel

> A row of slides on a native scroll-snap track, with frosted prev and next buttons, dots and arrow keys.

Category: Layout and surfaces. Docs page: https://rime.mjsons.net/components/carousel

## Installation

```bash
npx shadcn@latest add KezLahd/rime/carousel
```

Carousel lays its children out as slides on a scroll-snap track, so swipe, trackpad and the keyboard all scroll the same native element with no dependency. perView shows several at once on wide screens (one on phones); the buttons fade out at the ends unless loop is on.

autoplay (off by default) advances on a timer, stops while the pointer or focus is inside and has its own pause button, as WCAG 2.2.2 requires. Reduced motion scrolls instantly.

## Usage

```tsx
import { Carousel } from "@/components/ui";

<Carousel aria-label="Featured projects" perView={3}>
  {projects.map((p) => <ProjectCard key={p.id} project={p} />)}
</Carousel>
```

## Props: Carousel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | One child per slide. |
| `aria-label` (required) | `string` |  | Names the carousel region: "Featured projects". |
| `perView` | `1 \| 2 \| 3 \| 4` | `1` | Slides visible at once on wide screens. Narrow screens show one. Default 1. |
| `gap` | `number` | `12` | Gap between slides in px. Default 12. |
| `controls` | `boolean` | `true` | Prev and next buttons. On by default. |
| `dots` | `boolean` | `true` | The dot row under the track. On by default. |
| `autoplay` | `number` |  | Advance every n ms. Off by default; pauses on hover and focus, and has a pause button. |
| `loop` | `boolean` | `false` | Start again from the first slide after the last. Default false. |
| `onIndexChange` | `(index: number) => void` |  |  |
| `className` | `string` |  |  |

## Examples

### One per view

```tsx
<Carousel aria-label="Projects">{slides}</Carousel>
```

### Three per view

```tsx
<Carousel aria-label="Projects" perView={3}>{slides}</Carousel>
```

## Theme tokens

Per-component tokens. Undeclared by default: the default below is the fallback the CSS uses. Set any of them at :root or on a wrapper to restyle every instance.

| Token | Default |
| --- | --- |
| `--carousel-gap` | `12px` |
| `--carousel-nav-radius` | `var(--r-full)` |
| `--carousel-per-view` | `1` |
| `--carousel-radius` | `var(--r-lg)` |
| `--r-circle` | `50%` |

System tokens it reads: `--brand`, `--dur`, `--ease`, `--focus-ring`, `--ink-brand`, `--ink-heading`, `--ink-secondary`, `--line-input`, `--neutral-border`, `--popover-blur`, `--popover-surface`, `--r-full`, `--r-lg`, `--r-sm`, `--recess-fill`, `--recess-fill-hover`, `--shadow-float`, `--surface-solid`, `--text-caption`.

## Accessibility

- A section with aria-roledescription="carousel" and a required aria-label; each slide is a group with aria-roledescription="slide" labelled "2 of 5".
- Left and Right arrows move a slide; the track is focusable; off-screen slides are inert so Tab never lands on a hidden card.
- The track is a polite live region while not autoplaying, so a change of slide is announced.

## Do

- Use it for browsable, optional content: featured items, screenshots.
- Show a partial next slide (perView 3 at narrower widths) so people see there is more.

## Don't

- Don't put the only copy of important information on slide two or later.
- Don't turn autoplay on for content people need to read.

Related: https://rime.mjsons.net/components/aspect-ratio, https://rime.mjsons.net/components/card
