# Carousel (/docs/carousel)



<ComponentPreview name="swiper" />

The carousel handles the gesture and the physics:

* the track follows the finger 1:1 and rubber-bands past the ends;
* a release lands on the card the speed points to, never more than one away;
* a quick flick always moves one card, however short the drag;
* the snap is a spring that keeps the speed of the release.

The carousel doesn't position anything. Every frame, each slide gets its distance from the middle as CSS variables, and your CSS decides what that looks like.

```tsx
import { CardCarousel } from "@danolekh/cardstock/carousel";

<CardCarousel.Root index={index} onIndexChange={setIndex}>
  <CardCarousel.Viewport aria-label="Cards" tabIndex={0}>
    <CardCarousel.Track>
      {cards.map((card, i) => (
        <CardCarousel.Slide key={card.id} index={i}>
          …
        </CardCarousel.Slide>
      ))}
    </CardCarousel.Track>
  </CardCarousel.Viewport>
  <CardCarousel.Previous />
  <CardCarousel.Next />
  {cards.map((card, i) => (
    <CardCarousel.Indicator key={card.id} index={i} />
  ))}
</CardCarousel.Root>;
```

## Slide variables [#slide-variables]

| Variable                 | Range   | Meaning                                       |
| ------------------------ | ------- | --------------------------------------------- |
| `--slide-offset`         | signed  | Slides from the middle: −1 is one to the left |
| `--slide-offset-clamped` | −1 … 1  | The same, clamped                             |
| `--slide-distance`       | 0 … 1   | How far from the middle, without the sign     |
| `--carousel-position`    | 0 … n−1 | On the root: the track's fractional index     |

Stack the slides in one grid cell and move each by its offset. This is the whole 3D look of the demo above:

```css
[data-slot="carousel-track"] {
  display: grid;
  transform-style: preserve-3d;
}
[data-slot="carousel-slide"] {
  grid-area: 1 / 1;
  transform: translateX(calc(var(--slide-offset) * (100% + 20px)))
    translateZ(calc(var(--slide-distance) * -60px)) rotateY(calc(var(--slide-offset-clamped) * -24deg))
    scale(calc(1 - var(--slide-distance) * 0.1));
  opacity: calc(1 - var(--slide-distance) * 0.4);
}
```

Tie anything else to `--carousel-position`, such as a backdrop colour that blends as you swipe.

<TypeTable
  type="{
  index: { type: &#x22;number&#x22;, description: &#x22;Controlled index.&#x22; },
  defaultIndex: { type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  onIndexChange: { type: &#x22;(index: number) => void&#x22; },
  snap: {
    type: &#x22;{ stiffness, damping, mass? } | false&#x22;,
    default: &#x22;{ stiffness: 320, damping: 34 }&#x22;,
    description: &#x22;The settling spring. False jumps.&#x22;,
  },
}"
/>

`CardCarousel.Track` also takes `flickVelocity` (px/s, default 500) and `elastic` (share of the finger's travel past the ends, default 0.18). The physics functions (`snapTarget`, `rubberBand`, `stepSpring`) are exported as pure functions.

## Slides and clicks [#slides-and-clicks]

* A click on a card that isn't current makes it current (`selectOnClick`).
* The click that ends a drag is swallowed, so a swipe never presses a button on a card.
* `data-active` marks the current slide. The others are `aria-hidden`, so take any buttons inside them out of the tab order.
