# Carousel

A snap-to-item scroller along a row or down a column, with optional looping, autoplay and controls of your own.

```slint
import { Carousel } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 520px;
    height: 300px;
    background: Tokens.color-background;

    property <[string]> slides: ["1", "2", "3", "4", "5"];

    VerticalLayout {
        padding: 24px;
        alignment: center;

        Carousel {
            accessible-label: "Featured slides";
            height: 220px;
            count: root.slides.length;

            for slide in root.slides: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text {
                    text: slide;
                    color: Tokens.color-foreground;
                    font-size: Tokens.typography-headline-size;
                    font-weight: Tokens.typography-weight-semibold;
                }
            }
        }
    }
}
```

## Usage

```slint
import { Carousel } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component AppWindow inherits Window {
    width: 520px;
    height: 300px;
    background: Tokens.color-background;

    in property <[string]> slides;

    VerticalLayout {
        padding: 24px;

        c := Carousel {
            accessible-label: "Featured slides";
            count: root.slides.length;
            changed(index) => { debug("now showing", index); }

            for slide in root.slides: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text { text: slide; color: Tokens.color-foreground; }
            }
        }

        // Indicators, driven by `current`.
        HorizontalLayout {
            alignment: center;
            spacing: 8px;
            for i in root.slides.length: Rectangle {
                width: 8px;
                height: 8px;
                border-radius: 4px;
                background: Tokens.color-primary;
                opacity: i == c.current ? 1.0 : 0.3;
            }
        }
    }
}
```

Slint has one `@children` slot and cannot count what lands in it, so `count` is passed the way `ScrollArea` takes `viewport-height`. Every settled scroll position lands on an item boundary, and `current` is the index that boundary belongs to — two-way, so writing it steps the carousel and `changed` reports the steps the user makes.

**Items are laid out by the track, so they must be free to stretch**: give an item a fixed size and it stops lining up with the snap points. `item-length` sets that size for all of them at once, and it defaults to the whole viewport — one item at a time.

`CarouselColumn` is the same carousel stacked: it snaps on Y, answers `Up` / `Down` and puts its controls above and below the items. It is a component of its own rather than an `orientation` property because a `@children` slot is written once and never inside an `if` — the axis is structural, the way it is for `ButtonGroupColumn`, `Sheet` and `Drawer` (ADR-0027). Everything below is true of both.

## Examples

### A column

```slint
import { CarouselColumn } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 420px;
    height: 340px;
    background: Tokens.color-background;

    property <[string]> steps: ["Plan", "Build", "Review", "Ship"];

    VerticalLayout {
        padding: 24px;
        alignment: center;

        CarouselColumn {
            accessible-label: "Release steps";
            width: 280px;
            height: 260px;
            count: root.steps.length;

            for step in root.steps: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text {
                    text: step;
                    color: Tokens.color-foreground;
                    font-size: Tokens.typography-title-size;
                    font-weight: Tokens.typography-weight-semibold;
                }
            }
        }
    }
}
```

### Peeking neighbors

Shrink `item-length` below the viewport and the neighboring items show at the edges. `align` decides where the current item comes to rest: `CarouselAlign.start` leaves it flush with the leading edge, and `CarouselAlign.center` centers it with its neighbors peeking in on both sides — the carousel reserves half a viewport at each end so the first and last items can reach the middle. The built-in controls sit against the carousel’s own edges, which is where the peeking items are, so this one steps by drag and by keyboard instead.

```slint
import { Carousel, CarouselAlign } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 300px;
    background: Tokens.color-background;

    property <[string]> covers: ["A", "B", "C", "D", "E", "F"];

    VerticalLayout {
        padding: 24px;
        alignment: center;

        Carousel {
            accessible-label: "Covers";
            height: 200px;
            count: root.covers.length;
            align: CarouselAlign.center;
            item-length: 180px;
            spacing: 12px;
            controls: false;

            for cover in root.covers: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text {
                    text: cover;
                    color: Tokens.color-foreground;
                    font-size: Tokens.typography-h1-size;
                    font-weight: Tokens.typography-weight-semibold;
                }
            }
        }
    }
}
```

### Looping and autoplay

`loop` wraps the ends into each other, so stepping past the last item lands on the first and the controls never run out. `autoplay-interval` steps the carousel on its own — it waits while the reader is on the track or a drag is under way, picks up again once they leave, and starts its wait over whenever anything else moves the carousel. A carousel with nowhere left to go stops rather than sitting on the last item pushing against the end.

```slint
import { Carousel } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 520px;
    height: 300px;
    background: Tokens.color-background;

    property <[string]> quotes: [
        "Ships on Tuesday", "Two compilers", "One artifact", "No shims",
    ];

    VerticalLayout {
        padding: 24px;
        alignment: center;

        Carousel {
            accessible-label: "Quotes";
            height: 200px;
            count: root.quotes.length;
            loop: true;
            autoplay-interval: 2s;

            for quote in root.quotes: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text {
                    text: quote;
                    color: Tokens.color-foreground;
                    font-size: Tokens.typography-title-size;
                }
            }
        }
    }
}
```

### Controls of your own

`controls: false` takes the built-in prev/next buttons off. `can-previous` and `can-next` say whether stepping in each direction would land on another item — a looping carousel can always step, as long as there is a second item to step to — and `next()`, `previous()` and `go(index)` are the doors your own controls knock on. `status-label` is what the track announces politely on every index change; override it to translate or to say something else.

```slint
import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";
import { Carousel } from "@glint/components/carousel.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 340px;
    background: Tokens.color-background;

    property <[string]> slides: ["One", "Two", "Three", "Four"];

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        c := Carousel {
            accessible-label: "Onboarding";
            height: 180px;
            count: root.slides.length;
            controls: false;

            for slide in root.slides: Rectangle {
                background: Tokens.color-surface-1;
                border-radius: Tokens.radius-lg;
                Text {
                    text: slide;
                    color: Tokens.color-foreground;
                    font-size: Tokens.typography-title-size;
                }
            }
        }

        HorizontalLayout {
            alignment: center;
            spacing: 12px;

            Button {
                variant: ButtonVariant.outline;
                size: ButtonSize.icon-sm;
                leading-icon: IconSet.ChevronLeft;
                accessible-label: "Previous slide";
                disabled: !c.can-previous;
                clicked => { c.previous(); }
            }

            for i in root.slides.length: Rectangle {
                width: 10px;
                height: 10px;
                y: (parent.height - self.height) / 2;
                border-radius: 5px;
                background: Tokens.color-primary;
                opacity: i == c.current ? 1.0 : 0.3;
            }

            Button {
                variant: ButtonVariant.outline;
                size: ButtonSize.icon-sm;
                leading-icon: IconSet.ChevronRight;
                accessible-label: "Next slide";
                disabled: !c.can-next;
                clicked => { c.next(); }
            }
        }
    }
}
```

## API Reference

### Properties

| Property              | Type               | Default                                              | Description                                                                                                                                                                                                                                                                                                              |
| --------------------- | ------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `count`               | `in int`           | `0`                                                  | Number of items handed to the slot — usually `model.length`.                                                                                                                                                                                                                                                             |
| `item-length`         | `in length`        | no default                                           | Length of a single item along the track; the whole viewport (one item at a time) by default, which each track sets on its own axis.                                                                                                                                                                                      |
| `spacing`             | `in length`        | `16px`                                               | Gap between two items.                                                                                                                                                                                                                                                                                                   |
| `current`             | `in-out int`       | `0`                                                  | Two-way; index of the item the track is snapped to.                                                                                                                                                                                                                                                                      |
| `controls`            | `in bool`          | `true`                                               | Show the built-in prev/next controls.                                                                                                                                                                                                                                                                                    |
| `loop`                | `in bool`          | `false`                                              | Wrap the ends into each other: stepping past the last item lands on the first, and the controls never run out.                                                                                                                                                                                                           |
| `align`               | `in CarouselAlign` | `CarouselAlign.start`                                | Where the current item rests in the viewport.                                                                                                                                                                                                                                                                            |
| `autoplay-interval`   | `in duration`      | `0ms`                                                | Step the carousel by itself, this often. `0ms` — the default — is a carousel that only ever moves when it is asked to; anything else waits while the reader is on the track and picks up again once they leave.                                                                                                          |
| `previous-label`      | `in string`        | `@tr("Previous item")`                               | Accessible name of the control that steps backwards.                                                                                                                                                                                                                                                                     |
| `next-label`          | `in string`        | `@tr("Next item")`                                   | Accessible name of the control that steps forwards.                                                                                                                                                                                                                                                                      |
| `status-label`        | `in string`        | `@tr("Item {} of {}", root.current + 1, root.count)` | Announced politely whenever `current` changes; override to translate.                                                                                                                                                                                                                                                    |
| `can-previous`        | `out bool`         | no default                                           | Whether stepping in each direction would land on another item — for consumer-drawn controls (`controls: false`). A looping carousel can always step, as long as there is a second item to step to.                                                                                                                       |
| `can-next`            | `out bool`         | no default                                           |                                                                                                                                                                                                                                                                                                                          |
| `track-window`        | `in length`        | no default                                           | The window onto the track, measured along the axis it runs.                                                                                                                                                                                                                                                              |
| `track-offset`        | `in length`        | no default                                           | Where the track stands right now — non-positive, 0 at the first item.                                                                                                                                                                                                                                                    |
| `track-hovered`       | `in bool`          | `false`                                              | Set by the track while the pointer is on it.                                                                                                                                                                                                                                                                             |
| `engaged`             | `out bool`         | no default                                           | True while the reader is on the carousel — the pointer on the track, or a drag under way. Autoplay waits for as long as it holds.                                                                                                                                                                                        |
| `stride`              | `out length`       | no default                                           | Distance between two item origins.                                                                                                                                                                                                                                                                                       |
| `track-length`        | `out length`       | no default                                           | The whole track: every item, and the gaps between them.                                                                                                                                                                                                                                                                  |
| `viewport-length`     | `out length`       | no default                                           | What the track's Flickable scrolls: the items, plus the room a centred carousel needs at either end. A Flickable never scrolls its content past its own edges, so the empty half-viewport beside the first and last items has to be part of the content — the padding the tracks below put at both ends of their layout. |
| `inset`               | `out length`       | no default                                           | How much room that is: half a viewport minus half an item, so the current item lands in the middle with its neighbours peeking in.                                                                                                                                                                                       |
| `dragging`            | `out bool`         | no default                                           | True while the user's own drag (or its inertia) drives the track, so the content follows the finger instead of easing behind it.                                                                                                                                                                                         |
| `track-focus-visible` | `in bool`          | `false`                                              | Set by the track from its own focus scope: the ring is drawn here so both tracks draw the same one.                                                                                                                                                                                                                      |

### Callbacks

| Callback             | Description                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `changed(int)`       | Fired with the new index whenever the carousel settles on another item.                                                                     |
| `track-rest(length)` | Asks the track to come to rest at this offset. Only the track can move itself — the Flickable is its own — so every snap goes out this way. |

### Functions

| Function                               | Description                                                                                                                                                                                   |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `go(index: int)`                       | Snap onto `index`, wrapped or clamped to the items that exist. Reports through `changed` only when the index actually moves.                                                                  |
| `next()`                               |                                                                                                                                                                                               |
| `previous()`                           |                                                                                                                                                                                               |
| `track-ends-key(text: string) -> bool` | The two keys both axes answer the same way: whichever arrows a track binds, Home and End belong to the base. True when the key was one of them, which is what the track returns `accept` for. |
| `track-dragged()`                      | The track reports its own gesture: a drag streams in continuously, and the settle timer fires once the stream stops, which is when the gesture (and its inertia) is over.                     |

### Enums

| Enum            | Values            |
| --------------- | ----------------- |
| `CarouselAlign` | `start`, `center` |

### CarouselColumn

The same carousel stacked: `↑` / `↓` step it, and the controls sit against the top and bottom edges. Everything but the track and the two keys it answers is shared with `Carousel`, which is why the two tables are the same table twice.

### Properties

| Property              | Type               | Default                                              | Description                                                                                                                                                                                                                                                                                                              |
| --------------------- | ------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `count`               | `in int`           | `0`                                                  | Number of items handed to the slot — usually `model.length`.                                                                                                                                                                                                                                                             |
| `item-length`         | `in length`        | no default                                           | Length of a single item along the track; the whole viewport (one item at a time) by default, which each track sets on its own axis.                                                                                                                                                                                      |
| `spacing`             | `in length`        | `16px`                                               | Gap between two items.                                                                                                                                                                                                                                                                                                   |
| `current`             | `in-out int`       | `0`                                                  | Two-way; index of the item the track is snapped to.                                                                                                                                                                                                                                                                      |
| `controls`            | `in bool`          | `true`                                               | Show the built-in prev/next controls.                                                                                                                                                                                                                                                                                    |
| `loop`                | `in bool`          | `false`                                              | Wrap the ends into each other: stepping past the last item lands on the first, and the controls never run out.                                                                                                                                                                                                           |
| `align`               | `in CarouselAlign` | `CarouselAlign.start`                                | Where the current item rests in the viewport.                                                                                                                                                                                                                                                                            |
| `autoplay-interval`   | `in duration`      | `0ms`                                                | Step the carousel by itself, this often. `0ms` — the default — is a carousel that only ever moves when it is asked to; anything else waits while the reader is on the track and picks up again once they leave.                                                                                                          |
| `previous-label`      | `in string`        | `@tr("Previous item")`                               | Accessible name of the control that steps backwards.                                                                                                                                                                                                                                                                     |
| `next-label`          | `in string`        | `@tr("Next item")`                                   | Accessible name of the control that steps forwards.                                                                                                                                                                                                                                                                      |
| `status-label`        | `in string`        | `@tr("Item {} of {}", root.current + 1, root.count)` | Announced politely whenever `current` changes; override to translate.                                                                                                                                                                                                                                                    |
| `can-previous`        | `out bool`         | no default                                           | Whether stepping in each direction would land on another item — for consumer-drawn controls (`controls: false`). A looping carousel can always step, as long as there is a second item to step to.                                                                                                                       |
| `can-next`            | `out bool`         | no default                                           |                                                                                                                                                                                                                                                                                                                          |
| `track-window`        | `in length`        | no default                                           | The window onto the track, measured along the axis it runs.                                                                                                                                                                                                                                                              |
| `track-offset`        | `in length`        | no default                                           | Where the track stands right now — non-positive, 0 at the first item.                                                                                                                                                                                                                                                    |
| `track-hovered`       | `in bool`          | `false`                                              | Set by the track while the pointer is on it.                                                                                                                                                                                                                                                                             |
| `engaged`             | `out bool`         | no default                                           | True while the reader is on the carousel — the pointer on the track, or a drag under way. Autoplay waits for as long as it holds.                                                                                                                                                                                        |
| `stride`              | `out length`       | no default                                           | Distance between two item origins.                                                                                                                                                                                                                                                                                       |
| `track-length`        | `out length`       | no default                                           | The whole track: every item, and the gaps between them.                                                                                                                                                                                                                                                                  |
| `viewport-length`     | `out length`       | no default                                           | What the track's Flickable scrolls: the items, plus the room a centred carousel needs at either end. A Flickable never scrolls its content past its own edges, so the empty half-viewport beside the first and last items has to be part of the content — the padding the tracks below put at both ends of their layout. |
| `inset`               | `out length`       | no default                                           | How much room that is: half a viewport minus half an item, so the current item lands in the middle with its neighbours peeking in.                                                                                                                                                                                       |
| `dragging`            | `out bool`         | no default                                           | True while the user's own drag (or its inertia) drives the track, so the content follows the finger instead of easing behind it.                                                                                                                                                                                         |
| `track-focus-visible` | `in bool`          | `false`                                              | Set by the track from its own focus scope: the ring is drawn here so both tracks draw the same one.                                                                                                                                                                                                                      |

### Callbacks

| Callback             | Description                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `changed(int)`       | Fired with the new index whenever the carousel settles on another item.                                                                     |
| `track-rest(length)` | Asks the track to come to rest at this offset. Only the track can move itself — the Flickable is its own — so every snap goes out this way. |

### Functions

| Function                               | Description                                                                                                                                                                                   |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `go(index: int)`                       | Snap onto `index`, wrapped or clamped to the items that exist. Reports through `changed` only when the index actually moves.                                                                  |
| `next()`                               |                                                                                                                                                                                               |
| `previous()`                           |                                                                                                                                                                                               |
| `track-ends-key(text: string) -> bool` | The two keys both axes answer the same way: whichever arrows a track binds, Home and End belong to the base. True when the key was one of them, which is what the track returns `accept` for. |
| `track-dragged()`                      | The track reports its own gesture: a drag streams in continuously, and the settle timer fires once the stream stops, which is when the gesture (and its inertia) is over.                     |

### Enums

| Enum            | Values            |
| --------------- | ----------------- |
| `CarouselAlign` | `start`, `center` |

**The rows named `track-*` are not for you.** Both components are one brain and two tracks: `CarouselBase` holds the index and the snap arithmetic, and each of these tells it how wide its window is and where it stands. Slint has no `protected`, so a member a base publishes for its own subclass is published to the call site as well — and the call site’s binding wins. `track-window: 100px` compiles and lies to the arithmetic. The prefix is the whole guard rail (ADR-0043): it cannot stop you, but nothing reaches into the machinery by accident.

## Accessibility

- **A labeled region.** The carousel is one stop in the reading order carrying `accessible-role: region`; override `accessible-label` to name it, because “Carousel” tells a reader which widget it is and not what is in it.
- **The track is a live list.** It wears `accessible-role: list` with the item count and the axis it runs along, and a polite live region that announces `status-label` — “Item 3 of 5” by default — on every index change, without interrupting whatever the reader is doing.
- **The controls are named and guarded.** `previous-label` and `next-label` name the built-in buttons, which report as disabled at the ends of a carousel that does not loop and carry a default action so assistive technology never needs the pointer.
- **Keyboard.** `Tab` focuses the carousel; the arrows along its axis step — `Left` / `Right` for `Carousel`, `Up` / `Down` for `CarouselColumn` — and `Home` / `End` jump to the ends. The focus scope wraps the built-in controls rather than sitting beside them, so `Tab` moves from the carousel onto its own buttons and the arrows keep stepping from there.
- **Autoplay waits for the reader.** The pointer resting on the track, or a drag under way, holds the timer — so a carousel does not move out from under someone reading it — and any deliberate step restarts the wait rather than stepping again a moment later.
- **A single item scrolls nowhere.** The track stops being interactive below two items, so there is no gesture that pretends to have somewhere to go.
