# Carousel

A row of slides that scrolls and snaps one at a time.

Source: [carousel.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/carousel.tsx)

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";

const slides = [1, 2, 3, 4, 5];

export default function CarouselBasic() {
  return (
    <Carousel className="w-full max-w-sm">
      <CarouselContent>
        {slides.map((slide) => (
          <CarouselItem key={slide} aria-label={`${slide} of ${slides.length}`}>
            <div className="flex aspect-[3/2] items-center justify-center rounded-lg bg-layer-2 text-3xl font-medium text-muted-foreground shadow-[inset_0_0_0_0.5px_var(--border)]">
              {slide}
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/carousel
```

## Usage

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";

export function ScreenshotCarousel() {
  return (
    <Carousel>
      <CarouselContent>
        <CarouselItem>One</CarouselItem>
        <CarouselItem>Two</CarouselItem>
        <CarouselItem>Three</CarouselItem>
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}
```

The carousel uses native scrolling with CSS scroll snapping, so it needs no extra packages. The
arrow keys move it one slide at a time while focus is inside it.

Each slide fills the carousel. Give `CarouselItem` a `basis-*` class to show more than one, and set
`--carousel-gap` on the carousel to change the space between them.

Give each slide an `aria-label` such as "2 of 5" so screen readers can say where they are.

## Composition

- `Carousel`
  - `CarouselContent`
    - `CarouselItem`
  - `CarouselPrevious`
  - `CarouselNext`
  - `CarouselDots`

## Examples

### Several in view

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";
import { gradientImage } from "@/examples/data";

const covers = [
  { title: "Blue and green", image: gradientImage("#7698fd", "#6bd586", 160, 160) },
  { title: "Yellow and red", image: gradientImage("#f2cf76", "#f17471", 160, 160) },
  { title: "Light and dark blue", image: gradientImage("#a2bcff", "#3b5cf6", 160, 160) },
  { title: "Pale and deep green", image: gradientImage("#b8e9c1", "#198b43", 160, 160) },
  { title: "Cream and gold", image: gradientImage("#f7e5b5", "#cb9f34", 160, 160) },
  { title: "Pink and crimson", image: gradientImage("#f2bbb7", "#b82d35", 160, 160) },
];

export default function CarouselSeveral() {
  return (
    <Carousel className="w-full max-w-xl">
      <CarouselContent>
        {covers.map((cover, index) => (
          <CarouselItem
            key={cover.title}
            className="basis-1/2 sm:basis-1/3"
            aria-label={`${index + 1} of ${covers.length}`}
          >
            <img src={cover.image} alt={cover.title} className="aspect-square w-full rounded-lg" />
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}
```

### Dots

`CarouselDots` adds a dot for each position.

```tsx
import { Carousel, CarouselContent, CarouselDots, CarouselItem } from "@/components/ui/carousel";
import { gradientImage } from "@/examples/data";

const photos = [
  { alt: "Blue and green", image: gradientImage("#7698fd", "#6bd586", 480, 320) },
  { alt: "Yellow and red", image: gradientImage("#f2cf76", "#f17471", 480, 320) },
  { alt: "Light and dark blue", image: gradientImage("#a2bcff", "#3b5cf6", 480, 320) },
  { alt: "Pale and deep green", image: gradientImage("#b8e9c1", "#198b43", 480, 320) },
];

export default function CarouselWithDots() {
  return (
    <Carousel className="w-full max-w-sm">
      <CarouselContent>
        {photos.map((photo, index) => (
          <CarouselItem key={photo.alt} aria-label={`${index + 1} of ${photos.length}`}>
            <img src={photo.image} alt={photo.alt} className="aspect-[3/2] w-full rounded-lg" />
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselDots className="absolute inset-x-0 bottom-3 *:bg-white/50 *:data-active:bg-white" />
    </Carousel>
  );
}
```

### Controls below

Placed directly inside the carousel, `CarouselPrevious` and `CarouselNext` sit on either side of
the slides. Put them in another element to lay them out yourself, such as in a row with the dots.

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";

const tips = [
  { title: "Invite your team", body: "Add people from Settings, then Members." },
  { title: "Connect a repository", body: "Link GitHub to see commits on tasks." },
  { title: "Set up alerts", body: "Choose which updates reach your inbox." },
];

export default function CarouselControls() {
  return (
    <Carousel className="w-full max-w-sm">
      <CarouselContent>
        {tips.map((tip, index) => (
          <CarouselItem key={tip.title} aria-label={`${index + 1} of ${tips.length}`}>
            <div className="flex h-full flex-col gap-1 rounded-lg bg-layer-1 p-4 shadow-[inset_0_0_0_0.5px_var(--border-muted)]">
              <p className="text-sm font-medium">{tip.title}</p>
              <p className="text-sm text-muted-foreground">{tip.body}</p>
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <div className="mt-3 flex items-center justify-between">
        <CarouselDots />
        <div className="flex gap-2">
          <CarouselPrevious />
          <CarouselNext />
        </div>
      </div>
    </Carousel>
  );
}
```

### Vertical

Set `orientation="vertical"` to scroll up and down, and give `CarouselContent` a height.

```tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";

const slides = [1, 2, 3, 4, 5];

export default function CarouselVertical() {
  return (
    <Carousel orientation="vertical" className="w-full max-w-xs">
      <CarouselContent className="h-56">
        {slides.map((slide) => (
          <CarouselItem
            key={slide}
            className="basis-1/2"
            aria-label={`${slide} of ${slides.length}`}
          >
            <div className="flex h-full items-center justify-center rounded-lg bg-layer-2 text-2xl font-medium text-muted-foreground shadow-[inset_0_0_0_0.5px_var(--border)]">
              {slide}
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  );
}
```

## Props

`useCarousel()` gives your own parts inside the carousel its state and controls: `index`, `count`,
`canScrollPrev`, `canScrollNext`, `scrollTo(index)`, `scrollPrev()` and `scrollNext()`.

| Part | Extends |
| --- | --- |
| `Carousel` | `<div>` |
| `CarouselContent` | `<div>` |
| `CarouselItem` | `<div>` |
| `CarouselPrevious` | [`Button`](https://ui.jarv.is/components/button.md) |
| `CarouselNext` | [`Button`](https://ui.jarv.is/components/button.md) |
| `CarouselDots` | `<div>` |

### Carousel

| Prop           | Type                         | Description                                                                                                        |
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `orientation?` | `"horizontal" \| "vertical"` | The direction the slides scroll in. Vertical carousels need a height on `CarouselContent`. Default: `"horizontal"` |

### CarouselPrevious

| Prop                     | Type                                                                                 | Description                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `focusableWhenDisabled?` | `boolean`                                                                            | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
| `nativeButton?`          | `boolean`                                                                            | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |
| `variant?`               | `"destructive" \| "ghost" \| "link" \| "outline" \| "primary" \| "secondary"`        | Default: `"secondary"`                                                                                                                                                                            |
| `size?`                  | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs"` | Default: `"icon-sm"`                                                                                                                                                                              |

### CarouselNext

| Prop                     | Type                                                                                 | Description                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `focusableWhenDisabled?` | `boolean`                                                                            | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
| `nativeButton?`          | `boolean`                                                                            | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |
| `variant?`               | `"destructive" \| "ghost" \| "link" \| "outline" \| "primary" \| "secondary"`        | Default: `"secondary"`                                                                                                                                                                            |
| `size?`                  | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs"` | Default: `"icon-sm"`                                                                                                                                                                              |
