# Carousel

> Touch-friendly content rotator with autoplay, indicators, keyboard navigation, and reduced-motion support.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://www.sisyphosui.com/docs/components/carousel
- WAI-ARIA pattern: [Carousel](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/)

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { Carousel } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

```tsx
import { Carousel } from "@sisyphos-ui/react";

export const Slides = () => (
  <Carousel autoPlay>
    <img src="/a.jpg" alt="" />
    <img src="/b.jpg" alt="" />
    <img src="/c.jpg" alt="" />
  </Carousel>
);
```

### Vue 3+

```vue
<script setup lang="ts">
import { Carousel } from "@sisyphos-ui/vue";
</script>

<template>
  <Carousel :auto-play="true">
    <img src="/a.jpg" alt="" />
    <img src="/b.jpg" alt="" />
    <img src="/c.jpg" alt="" />
  </Carousel>
</template>
```

### Angular 17+

```ts
import { Component } from "@angular/core";
import { Carousel } from "@sisyphos-ui/angular";

@Component({
  selector: "app-slides",
  standalone: true,
  imports: [Carousel],
  template: `
    <sui-carousel [items]="photos" autoPlay>
      <ng-template let-photo>
        <img [src]="photo.src" [alt]="photo.alt" />
      </ng-template>
    </sui-carousel>
  `,
})
export class SlidesComponent {
  photos = [
    { src: "/a.jpg", alt: "" },
    { src: "/b.jpg", alt: "" },
  ];
}
```

## Examples

### Default

```tsx
import { Carousel } from "@sisyphos-ui/react";

const SLIDES = [
  { bg: "#ff7022",  title: "Ship faster" },
  { bg: "#10b981",  title: "Stay accessible" },
  { bg: "#3b82f6",  title: "Theme at runtime" },
];

export function Example() {
  return (
    <Carousel className="w-full max-w-xl">
      {SLIDES.map((s) => (
        <div
          key={s.title}
          style={{ background: s.bg }}
          className="flex aspect-[16/9] items-center justify-center rounded-lg text-white text-2xl font-bold"
        >
          {s.title}
        </div>
      ))}
    </Carousel>
  );
}
```

### Auto-play

Enable `autoPlay` and tune the pace with `autoPlayInterval`. Pauses on hover/focus.

```tsx
import { Carousel } from "@sisyphos-ui/react";

const SLIDES = [
  { bg: "#ff7022", title: "Ship faster" },
  { bg: "#10b981", title: "Stay accessible" },
  { bg: "#3b82f6", title: "Theme at runtime" },
];

function Slide({ bg, title }: { bg: string; title: string }) {
  return (
    <div
      style={{
        background: bg,
        aspectRatio: "16 / 9",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "#fff",
        fontWeight: 700,
        fontSize: 28,
        borderRadius: 12,
      }}
    >
      {title}
    </div>
  );
}

export function Example() {
  return (
    <Carousel autoPlay autoPlayInterval={2500}>
      {SLIDES.map((s) => <Slide key={s.title} {...s} />)}
    </Carousel>
  );
}
```

### No loop

`loop={false}` stops wrapping — arrows disable at the edges.

```tsx
import { Carousel } from "@sisyphos-ui/react";

const SLIDES = [
  { bg: "#ff7022", title: "Ship faster" },
  { bg: "#10b981", title: "Stay accessible" },
  { bg: "#3b82f6", title: "Theme at runtime" },
];

function Slide({ bg, title }: { bg: string; title: string }) {
  return (
    <div
      style={{
        background: bg,
        aspectRatio: "16 / 9",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "#fff",
        fontWeight: 700,
        fontSize: 28,
        borderRadius: 12,
      }}
    >
      {title}
    </div>
  );
}

export function Example() {
  return (
    <Carousel loop={false} defaultIndex={1}>
      {SLIDES.map((s) => <Slide key={s.title} {...s} />)}
    </Carousel>
  );
}
```

### Dots only

Hide arrow controls with `showArrows={false}` for pure dot-driven navigation.

```tsx
import { Carousel } from "@sisyphos-ui/react";

const SLIDES = [
  { bg: "#ff7022", title: "Ship faster" },
  { bg: "#10b981", title: "Stay accessible" },
  { bg: "#3b82f6", title: "Theme at runtime" },
];

function Slide({ bg, title }: { bg: string; title: string }) {
  return (
    <div
      style={{
        background: bg,
        aspectRatio: "16 / 9",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "#fff",
        fontWeight: 700,
        fontSize: 28,
        borderRadius: 12,
      }}
    >
      {title}
    </div>
  );
}

export function Example() {
  return (
    <Carousel showArrows={false}>
      {SLIDES.map((s) => <Slide key={s.title} {...s} />)}
    </Carousel>
  );
}
```

### Controlled index

Drive the carousel from outside UI with `index` + `onIndexChange`.

```tsx
import { useState } from "react";
import { Button, Carousel } from "@sisyphos-ui/react";

const SLIDES = [
  { bg: "#ff7022", title: "Ship faster" },
  { bg: "#10b981", title: "Stay accessible" },
  { bg: "#3b82f6", title: "Theme at runtime" },
];

function Slide({ bg, title }: { bg: string; title: string }) {
  return (
    <div
      style={{
        background: bg,
        aspectRatio: "16 / 9",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "#fff",
        fontWeight: 700,
        fontSize: 28,
        borderRadius: 12,
      }}
    >
      {title}
    </div>
  );
}

export function Example() {
  const [index, setIndex] = useState(0);

  return (
    <>
      <Button disabled={index === 0} onClick={() => setIndex(i => i - 1)}>Prev</Button>
      <Button disabled={index === SLIDES.length - 1} onClick={() => setIndex(i => i + 1)}>Next</Button>
      <Carousel index={index} onIndexChange={setIndex} loop={false}>
        {SLIDES.map((s) => <Slide key={s.title} {...s} />)}
      </Carousel>
    </>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| index | `number` | — | Controlled active slide index. |
| defaultIndex | `number` | `0` | Initial slide index when uncontrolled. |
| onIndexChange | `(index: number) => void` | — | Called with the new index on every slide change. |
| autoPlay | `boolean` | `false` | Auto-advance every `autoPlayInterval` ms. |
| autoPlayInterval | `number` | `5000` | Time between auto-advances in ms. |
| loop | `boolean` | `true` | Wrap from the last slide back to the first; when off, arrows disable at the edges. |
| showArrows | `boolean` | `true` | Render previous / next arrow buttons (hidden with a single slide). |
| showDots | `boolean` | `true` | Render dot indicators below the viewport. |
| pauseOnHover | `boolean` | `true` | Pause autoplay on mouse enter or focus. |
| ariaLabel | `string` | `"carousel"` | Value applied to the container's `aria-roledescription`. |

## Keyboard interactions

- **ArrowRight** — Advances to the next slide while the carousel region is focused.
- **ArrowLeft** — Goes back to the previous slide.
- **Tab** — Moves focus from the region to the arrow buttons and dot indicators.

## Accessibility notes

- The container is a focusable `role="region"` (`tabIndex={0}`) with `aria-roledescription` (default `"carousel"`, overridable via `ariaLabel`).
- Each slide is `role="group"` with `aria-roledescription="slide"` and an `aria-label` of `"n of total"`; off-screen slides are `aria-hidden="true"`.
- Dot indicators form a `role="tablist"` labelled "Slide selector"; each dot is a `role="tab"` with `aria-selected` and a "Go to slide n" label.
- Arrow buttons carry "Previous slide" / "Next slide" `aria-label`s and disable at the edges when `loop` is off.
- Autoplay pauses while the carousel is hovered or focused (`pauseOnHover`).

<!-- exports: { "Carousel": "@sisyphos-ui/react" } -->