Skip to content
Glint UI

Carousel

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

Usage

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

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.

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.

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.

API Reference

Properties

PropertyTypeDefaultDescription
countin int0Number of items handed to the slot — usually model.length.
item-lengthin lengthno defaultLength 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.
spacingin length16pxGap between two items.
currentin-out int0Two-way; index of the item the track is snapped to.
controlsin booltrueShow the built-in prev/next controls.
loopin boolfalseWrap the ends into each other: stepping past the last item lands on the first, and the controls never run out.
alignin CarouselAlignCarouselAlign.startWhere the current item rests in the viewport.
autoplay-intervalin duration0msStep 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-labelin string@tr("Previous item")Accessible name of the control that steps backwards.
next-labelin string@tr("Next item")Accessible name of the control that steps forwards.
status-labelin string@tr("Item {} of {}", root.current + 1, root.count)Announced politely whenever current changes; override to translate.
can-previousout boolno defaultWhether 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-nextout boolno default
track-windowin lengthno defaultThe window onto the track, measured along the axis it runs.
track-offsetin lengthno defaultWhere the track stands right now — non-positive, 0 at the first item.
track-hoveredin boolfalseSet by the track while the pointer is on it.
engagedout boolno defaultTrue 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.
strideout lengthno defaultDistance between two item origins.
track-lengthout lengthno defaultThe whole track: every item, and the gaps between them.
viewport-lengthout lengthno defaultWhat 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.
insetout lengthno defaultHow much room that is: half a viewport minus half an item, so the current item lands in the middle with its neighbours peeking in.
draggingout boolno defaultTrue 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-visiblein boolfalseSet by the track from its own focus scope: the ring is drawn here so both tracks draw the same one.

Callbacks

CallbackDescription
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

FunctionDescription
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) -> boolThe 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

EnumValues
CarouselAlignstart, 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

PropertyTypeDefaultDescription
countin int0Number of items handed to the slot — usually model.length.
item-lengthin lengthno defaultLength 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.
spacingin length16pxGap between two items.
currentin-out int0Two-way; index of the item the track is snapped to.
controlsin booltrueShow the built-in prev/next controls.
loopin boolfalseWrap the ends into each other: stepping past the last item lands on the first, and the controls never run out.
alignin CarouselAlignCarouselAlign.startWhere the current item rests in the viewport.
autoplay-intervalin duration0msStep 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-labelin string@tr("Previous item")Accessible name of the control that steps backwards.
next-labelin string@tr("Next item")Accessible name of the control that steps forwards.
status-labelin string@tr("Item {} of {}", root.current + 1, root.count)Announced politely whenever current changes; override to translate.
can-previousout boolno defaultWhether 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-nextout boolno default
track-windowin lengthno defaultThe window onto the track, measured along the axis it runs.
track-offsetin lengthno defaultWhere the track stands right now — non-positive, 0 at the first item.
track-hoveredin boolfalseSet by the track while the pointer is on it.
engagedout boolno defaultTrue 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.
strideout lengthno defaultDistance between two item origins.
track-lengthout lengthno defaultThe whole track: every item, and the gaps between them.
viewport-lengthout lengthno defaultWhat 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.
insetout lengthno defaultHow much room that is: half a viewport minus half an item, so the current item lands in the middle with its neighbours peeking in.
draggingout boolno defaultTrue 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-visiblein boolfalseSet by the track from its own focus scope: the ring is drawn here so both tracks draw the same one.

Callbacks

CallbackDescription
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

FunctionDescription
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) -> boolThe 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

EnumValues
CarouselAlignstart, 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.