<!-- Protocore Design System — Carousel -->
# Carousel

- **Category:** Media (`media`)
- **Slug:** `media/carousel`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Carousel } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/media/carousel

> Accessible slide carousel — a scroll-snap track with mono prev/next buttons, square dot indicators, arrow-key navigation and optional autoplay.

## When to use it

Use **Carousel** for a small, browsable set of peer panels where showing one at a time is intentional — a hero rotation, a testimonial reel, a screenshot gallery. It degrades gracefully: the track is a native CSS scroll-snap container, so slides remain swipeable even if JavaScript never loads.

- Carrying **navigation between views** of one entity? That's **Tabs**, not a carousel.
- Showing a **continuous logo strip**? Use **LogoMarquee**.
- Hiding essential content behind autoplay is an anti-pattern — keep autoplay for ambient, non-critical panels and never bury a primary CTA on slide 3.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | `Carousel` | Accessible name for the carousel region. Default `"Carousel"`. |
| `autoPlay` | `boolean` | no | `false` | Advance automatically. Paused on hover/focus and disabled under reduced-motion. Default `false`. |
| `children` | `ReactNode` | yes | — | `Carousel.Slide` children. |
| `className` | `string` | no | — | — |
| `defaultIndex` | `number` | no | `0` | Initial slide index in uncontrolled mode. Default `0`. |
| `hideControls` | `boolean` | no | `false` | Hide the prev/next/dots control row (e.g. for a swipe-only mobile track). Default `false`. |
| `index` | `number` | no | — | Active slide index (controlled). |
| `interval` | `number` | no | `5000` | Autoplay dwell time per slide, in ms. Default `5000`. |
| `loop` | `boolean` | no | `false` | Wrap from last→first (and first→last) instead of stopping at the ends. Default `false`. |
| `onIndexChange` | `((index: number) => void)` | no | — | Fires whenever the active slide changes (both modes). |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Basics

Wrap each panel in a `Carousel.Slide`. The region gets `aria-roledescription="carousel"`; prev/next buttons and a dot per slide are drawn for you. Give the region a name via `aria-label`.

```tsx
import { Carousel, Card } from "@protocore/pds";

const HIGHLIGHTS = [
  {
    tag: "eu-central-1",
    title: "Frankfurt region live",
    body: "Static sites now serve from an EU edge — sub-40ms TTFB across the Rhine-Main corridor.",
  },
  {
    tag: "cms",
    title: "Multi-tenant CMS shipped",
    body: "One Payload instance drives every customer site; tenants see only their own content.",
  },
  {
    tag: "ses",
    title: "Production email access",
    body: "Transactional mail leaves SES sandbox — order receipts send from noreply@protocore.io.",
  },
];

export default function CarouselBasics() {
  return (
    <div style={{ maxWidth: 520 }}>
      <Carousel.Root aria-label="Release highlights">
        {HIGHLIGHTS.map((h, i) => (
          <Carousel.Slide key={h.tag}>
            <Card index={i + 1} title={h.title} subtitle={h.tag}>
              {h.body}
            </Card>
          </Carousel.Slide>
        ))}
      </Carousel.Root>
    </div>
  );
}
```

### Looping autoplay

Set `autoPlay` with `loop` to rotate continuously. Autoplay pauses while the pointer is over the carousel or focus is inside it, and is disabled entirely under `prefers-reduced-motion`.

```tsx
import { Carousel, Panel, Heading, Text } from "@protocore/pds";

const QUOTES = [
  {
    who: "Eustatiu · unghiuțe la minut",
    quote:
      "Launched the whole storefront off one CMS tenant. Content edits go live without a redeploy.",
  },
  {
    who: "Roxana · trasor.io",
    quote: "The design system did the heavy lifting — every page reads like one product.",
  },
  {
    who: "Ops · protocore.io",
    quote: "Two AWS accounts, one deploy script. Cost isolation without the operational tax.",
  },
];

export default function CarouselAutoplay() {
  return (
    <div style={{ maxWidth: 520 }}>
      <Carousel.Root aria-label="What teams say" autoPlay loop interval={4000}>
        {QUOTES.map((q) => (
          <Carousel.Slide key={q.who}>
            <Panel>
              <Heading as="h3" size="title">
                “{q.quote}”
              </Heading>
              <Text color="muted" mono size="sm">
                {q.who}
              </Text>
            </Panel>
          </Carousel.Slide>
        ))}
      </Carousel.Root>
    </div>
  );
}
```

## Do & don't

**Do**

- Give the carousel an accessible name with `aria-label`.
- Keep autoplay for ambient content; it already pauses on hover/focus and respects reduced-motion.
- Use `loop` when the set is a true rotation with no natural start or end.
- Keep each slide's content self-contained so any slide reads on its own.

**Don't**

- Don't autoplay content users must read or act on — they can't keep pace.
- Don't put your only copy of a primary action inside a non-first slide.
- Don't use a carousel where a simple grid or stack would show everything at once.
- Don't remove the dots and arrows — they're the visible, keyboard-reachable controls.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Arrow Left / Right` | Move to the previous / next slide. |
| `Home / End` | Jump to the first / last slide. |
| `Tab` | Move into the prev/next buttons and the dot controls. |
| `Enter / Space` | Activate the focused control. |

**Notes**

- The region carries `aria-roledescription="carousel"`; each slide is a `role="group"` labelled "n of m" (override per slide with your own `aria-label`).
- The slide track is an `aria-live` region — polite while paused, off while autoplaying — so slide changes are announced without fighting a running rotation.
- Prev/next arrows are inline SVGs (never emoji glyphs); the active dot uses the reserved accent as the current-slide marker.
- Autoplay honours `prefers-reduced-motion` (no rotation) and pauses on hover and on focus within the carousel.

## Related

`aspect-ratio`, `tabs`, `logo-marquee`, `product-card`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
