> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/carousel-container.html
> Source: specs/components/carousel-container/carousel-container.md, specs/components/carousel-container/carousel-container-a11y.md, specs/components/carousel-container/capabilities.json

---

# CarouselContainer · Oymyakon DS 3

> A horizontally scrolling container for a row of DS components — promo banners, paged slides,
> home-screen shelves. Item width is computed from the container's width; every offset is an SP token.

**Version:** 3.0.2 · **Status:** Ready · **Figma:** `[CarouselContainer] 3.0` (main components file `7zltjwJiJP7P9qYPoeTGiW`, page `37827:3312`; component `37890:33399` — a `Type` instance swap over the Peek, Paged and Shelf sets)

**Source of this version:** the design playground *CarouselContainer 3.0 Playground* (owner, 2026-11-01) —
the parameters, the geometry, the snapping, the indicator, autoplay and the screen-reader phrases come
from it. The geometry follows the Figma component `[CarouselContainer] 3.0` (owner, 2026-11-02); where the
two still disagree, the disagreement is listed in § 12 rather than resolved here.

---

## 1. Description

CarouselContainer lays a row of items out horizontally and lets the user scroll it. It owns the
row — width, insets, gaps, snapping, the position indicator and autoplay — and nothing inside the
items: each item is a `CarouselItem` whose content slot takes any DS component (a promo banner, a
Card, a city card, a category tile).

Three modes cover the product's horizontal rows:

| Mode | For | What the user sees |
|---|---|---|
| **Peek** (default) | Promo banners, featured cards | One (or two) items per view, the next item peeking in by `s28` |
| **Paged** | Full-width slides | One item per page, dots under the row or over it (`pageIndicator`: below by default, overlay, or none) |
| **Shelf** | Home-screen shelves: categories, rides, cities | Fixed-width items, free scroll, the last visible item cut by the edge |

The item width is computed from the container's width, so one configuration serves every
screen size — 304 on a 360 screen in Peek, 374 on 430.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | — | — | Not in the platform bridge (manifest seed, 2026-11-01) |
| Android | — | — | Not in the platform bridge (manifest seed, 2026-11-01) |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.carousel-container`) + `DS.initCarousel` in `src/shared/shared.js` | 3.0.0 | Reference implementation, this repo; preview page `src/carousel-container.njk`. Instance props `--cc-edge`, `--cc-gap`, `--cc-peek`, `--cc-per-view`, `--cc-item-max`, `--cc-shelf-width`; modifiers `--paged`, `--shelf`, `--single`; autoplay via `data-autoplay="<ms>"` + `data-loop`; `initialIndex` via `data-start-index`; `pageIndicator: below` = `.carousel-container__dots`, `overlay` = `+ .carousel-container__dots--overlay`, `none` = the dots element omitted; `pageIndicatorAlign: start` / `end` = `.carousel-container__dots--start` / `--end`; `stretchItems` = `.carousel-container--stretch` |

---

## 2. Anatomy

```
CarouselContainer                       full container width · region · s8 top and bottom
├── Viewport                              clips the track to the container's width
│   └── Track                             horizontal row · padding-inline = edgeInset · gap · scroll + snap
│       └── CarouselItem × N              width per mode (§ 3) · stretches to the tallest item
│           └── Content slot              any DS component
└── Page indicator (dots)                 Paged only · centre (default), start or end · below the row (default) or over its bottom edge
```

| Part | Required | Description |
|---|---|---|
| **Container** | Required | The region the screen reader enters. Takes the full width of its parent; padding `s8` top and bottom; no fill, no border, no radius of its own |
| **Viewport** | Required | Clips the track at the container's edges. The peeking neighbour and a cut-off shelf item are cut here |
| **Track** | Required | The scrolling row. `edgeInset` on both ends (the first item starts and the last item ends `edgeInset` from the edge), `gap` between items, snapping per mode |
| **CarouselItem** | Required, 1…N | One cell of the row. Width per mode (§ 3); height follows the content — the row is as tall as its tallest item |
| **Content slot** | Required | Any DS component. The item has no fill or radius of its own — the content brings its own shape (a banner's radius, a card's surface) |
| **Page indicator** | Paged only — `pageIndicator`: `below` (default), `overlay`, `none` | One dot per item, the current one elongated, padded `s4`. Centred by default, or at the start / end (`pageIndicatorAlign`), `s12` in from the edge inset. **Below** — in a bottom slot under the row (`s4` above, `s8` below). **Overlay** — over the row's bottom edge, on a `Surface/WhiteOverlay` pill, `s8` above that edge; reserves no height. Decorative: not tappable, hidden from the screen reader — the item announces its position (§ 10) |

- A section title above the carousel ("Promotions ›") is a separate component (SectionHeader),
  not part of CarouselContainer. Its text is the default accessibility label of the container.
- Dots inside a card — where the card's content pages, not the card — are a different pattern and
  not CarouselContainer.

### RTL layout

Direction follows the locale; there is no separate parameter. In RTL the first item sits on the
right, the neighbour peeks in from the left, the track scrolls the other way, and the dots run
right to left. The content of each item follows its own component's RTL rules.

---

## 3. Variants and sizes

### Parameters

| Parameter | Type | Default | Values | Behaviour |
|---|---|---|---|---|
| `mode` | enum | `peek` | `peek` · `paged` · `shelf` | Sets the item width, the snapping and the indicator. Only `paged` has dots |
| `itemsPerView` | int | `1` | `1` · `2` | `peek` only. The item width is shared by N items per view |
| `itemWidth` | dp | `96` | 72–200 | `shelf` only. A fixed width; the height follows the content |
| `edgeInset` | SP token | `s20` | SP | The first item's offset from the start and the last item's from the end. Also the snap point |
| `gap` | SP token | `s8` | `s8` · `s4` | Spacing between items in every mode, between pages in `paged` too (§ 12, Q13) |
| `peek` | SP token | `s28` | SP | `peek` only. The visible part of the neighbouring item |
| `maxItemWidth` | dp | — | e.g. `320` | `peek` only. Caps the width of every item; when the cap applies, the neighbour peeks in by more than `s28`. The recommended level for a cap (tablets, wide screens) |
| `item.maxWidth` | dp | — | e.g. `320` | Caps one item. Breaks the row's rhythm — that item's neighbour peeks in by a different amount. For exceptions only |
| `items[]` | array | — | 1…N `CarouselItem` | Each item carries one content slot |
| `initialIndex` | int | `0` | 0…N−1 | The item the carousel opens on. The last item still aligns to the end (§ Snapping) |
| `autoplay` | bool | `false` | — | `paged` only. Off by default; stops at the first user interaction (§ 5) |
| `autoplayInterval` | ms | `7000` | 3000–10000 | How long one page shows |
| `loop` | bool | `false` | — | `paged` only: after the last page, the first |
| `pageIndicator` | enum | `below` | `below` · `overlay` · `none` | `paged` only. Where the dots sit (owner, 2026-11-06 — replaces the boolean). `below` — under the row, in a slot of its own. `overlay` — over the bottom edge of the page, on a `Surface/WhiteOverlay` pill, for dark or photo banners where the height counts (on a light banner the white 16% pill all but disappears); the row is as tall as its items. `none` — no dots and no space for them; the row has no visual cue that it scrolls — the item positions are still announced (§ 10). Figma: the `PageIndicator` variant (`Below` · `Overlay`) and the `🔴BottomSlot` boolean (off = `none`) |
| `pageIndicatorAlign` | enum | `center` | `center` · `start` · `end` | `paged` only, with `below` or `overlay` (owner, 2026-11-06). Where the indicator sits along the row: centred, or at the start / end edge, `s12` in from the edge inset (32 from the container edge at the default inset). Logical — start is the left edge in LTR and the right in RTL. Not yet a Figma property (§ 12, Q8) |
| `stretchItems` | bool | `false` | — | `shelf` only. By default items keep `itemWidth` even when they do not fill the row — the row starts at `edgeInset` and the rest stays empty. `true` stretches them equally to fill `W − 2 · edgeInset` when they fit; once they overflow, `itemWidth` applies and the row scrolls |
| `accessibilityLabel` | string | the section title | — | The container's name for the screen reader, without the word "carousel" (§ 10) |

### Item width per mode

`W` is the container's width, `N` the number of items per view.

| Mode | Item width | At W 360 | At W 430 |
|---|---|---|---|
| **Peek**, N = 1 | `W − edgeInset − gap − peek` | 304 | 374 |
| **Peek**, N = 2 | `(W − edgeInset − gap − peek − gap) / 2` | 148 | 183 |
| **Peek** with a cap | `min(formula, maxWidth)`; the neighbour then peeks in by `W − edgeInset − maxWidth − gap` | 304 (no cap) | 320 (peek 82) |
| **Paged** | `W − 2 · edgeInset` | 320 | 390 |
| **Shelf** | `itemWidth` | 96 (≈ 3.3 per view) | 96 (≈ 4.0 per view) |
| **One item** — Peek, Paged | `W − 2 · edgeInset` — no swipe, no indicator | 320 | 390 |
| **Few items** — Shelf (they fit in the row, one item included) | `itemWidth`, not stretched — the row ends where the items end; with `stretchItems` each takes `(W − 2 · edgeInset − (N − 1) · gap) / N` | 96 | 96 |

Heights follow the content — the Figma component sets no minimum; the row is as tall as its tallest
item, plus `s8` above and below.

### Snapping and scrolling

| Mode | Snapping | Per swipe |
|---|---|---|
| **Peek** | To the item's start, at `edgeInset` | One item |
| **Paged** | Per page; a fast fling stops on the next page (`scroll-snap-stop: always` on the web) | One page |
| **Shelf** | None — free scroll | Momentum |

The last item aligns to the end, `edgeInset` from the edge; in Peek the previous item then peeks
in from the start side by `peek`.

### Indicator

The row signals that it scrolls in one way — the peek, the dots or a cut-off item:

| Mode | Indicator |
|---|---|
| **Peek** | The peeking neighbour (`s28`) |
| **Paged** | Dots — under the row by default (`below`), or over its bottom edge on a pill (`overlay`); centred by default, at the start or end with `pageIndicatorAlign`; with `pageIndicator: none` the row has none |
| **Shelf** | The last visible item cut by the edge |

Dots count up to 7 items; past that, Shelf or a jump to a list fits the content better (playground rule).

---

## 4. States

The states differ by mode — the snapping, the indicator and autoplay are what change — so each mode
lists its own. The rest state is **Standard** in every mode.

### Peek

Snaps to the item's start at `edgeInset`, one item per swipe; the neighbour peeking in by `peek` is
the indicator. No dots, no autoplay.

| State | What shows | Behaviour |
|---|---|---|
| **Standard** | The first item at `edgeInset` from the start edge; the next item peeks in by `s28` at the end edge | At rest on a snap point |
| **Scrolling** | The row between snap points while the user drags; both neighbours partly visible | On release the row settles on the nearest item's start (one item per swipe) |
| **Item focused** | A middle item on its snap point; the next item peeks in by `s28` at the end edge, the previous one shows `edgeInset − gap` (12) at the start edge | Keyboard or screen-reader focus scrolls the item to its snap point; the focus ring is the content's own |
| **End of row** | The last item aligned to the end edge at `edgeInset`; the previous item peeks in from the start side by `s28` | The row cannot scroll further; there is no peek at the end edge |
| **Single item** | One item at `W − 2 · edgeInset` | No scrolling, no snapping, no peek |

### Paged

One item per page, spaced by `gap`; the dots are the indicator — below the row by default,
over it with `pageIndicator: overlay`, off with `none`. Autoplay is available in this mode only.

| State | What shows | Behaviour |
|---|---|---|
| **Standard** | Page 1 at `edgeInset` on both sides; the next page shows `edgeInset − gap` (12) at the end edge; the first dot elongated | At rest on a page |
| **Scrolling** | Two pages partly visible while the user drags; the dot changes when the row settles | A fling stops on the next page (`snap-stop`), never skips one |
| **Middle page** | Page N of M; the N-th dot elongated, the others `Surface/ContrastOverlay` | — |
| **Last page** | The last page; the last dot elongated | With `loop` off the row stops here; autoplay ends on it |
| **Autoplay running** | As Standard; the pages advance on their own every `autoplayInterval` | Pauses while the pointer hovers the row (web) |
| **Autoplay stopped** | The page the user stopped on | Stopped by the first interaction (one during an advance included), or held off by Reduce Motion or, on iOS / Android, an assistive technology; does not resume |
| **Indicator over the row** | As Standard; the dots on a `Surface/DarkOverlay` pill over the page (current `TextAndIcon/AlwaysLight`, others `Surface/WhiteOverlay`), `s8` above its bottom edge; no space below the row | Unchanged — the pill moves with nothing; it stays centred while the pages scroll under it |
| **Indicator at the start / end** | As Standard or Indicator over the row; the indicator at the start or end edge, `s12` in from the edge inset | Unchanged; in RTL start and end swap sides with the row |
| **Without page indicator** | As Standard, with no dots and no space reserved for them below the row | Unchanged — the page still snaps; nothing on screen shows the position |
| **Single item** | One page; no dots | No scrolling, no autoplay |

### Shelf

Fixed-width items, free scroll with no snapping; the item cut by the edge is the indicator. No dots,
no autoplay. Items keep their width when they do not fill the row — `stretchItems` stretches them.

| State | What shows | Behaviour |
|---|---|---|
| **Standard** | The first item at `edgeInset`; the last visible item cut by the end edge | At rest |
| **Scrolled** | The row at any offset; items cut on both edges | Free scroll — the row rests wherever the momentum ends |
| **Item focused** | A focused item scrolled fully into view | Keyboard or screen-reader focus scrolls the item into view; the focus ring is the content's own |
| **End of row** | The last item aligned to the end edge at `edgeInset` | The row cannot scroll further |
| **Few items** | Items that fit in the row keep `itemWidth`, starting at `edgeInset`; the rest of the row stays empty | No scrolling |
| **Few items, stretched** | With `stretchItems`, the items share `W − 2 · edgeInset` equally | No scrolling; once they overflow, `itemWidth` applies |
| **Single item** | One item at `itemWidth` (`W − 2 · edgeInset` with `stretchItems`) | No scrolling |

### Not states of the container

- No Pressed or Disabled state of its own: pressing and disabling belong to the item's content.
- No Skeleton state of its own: while loading, the host fills the items with the content's own
  Skeleton.

---

## 5. Animation and behavior

Reference: [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md). Swiping, flinging and
snapping are the platform's native scroll physics and are not tokenised.

| Event | Pattern | Token | Notes |
|---|---|---|---|
| Programmatic scroll — autoplay advance, focus moved to an off-screen item | moveM | `var(--component-move-m-appear)` | 400 ms on the standard curve (owner, 2026-11-01 — replaces the playground's 400 ms ease-in-out) |
| Dot change (current dot elongates) | Transforming/State | candidate `var(--transforming-state-default)` | The colour changes on the token; the width (`s6` → `s16`) is a layout property and is not animated |
| Swipe, fling, snap | Native scroll | — | Platform physics |

### Autoplay

- Off by default; available in Paged only.
- Stops at the first interaction with the carousel — a touch, a swipe, a wheel scroll, keyboard
  focus or screen-reader focus — and does not resume on its own. There is no pause control
  (owner, 2026-11-01).
- Is off under Reduce Motion, and on iOS / Android while an assistive technology (VoiceOver /
  TalkBack, Switch Control / Switch Access, Voice Control / Voice Access) is on. The web cannot
  detect a screen reader, so there it runs until an interaction stops it (a11y spec § Known gap).
- Pauses while the pointer hovers the row (web).
- With `loop` off it stops on the last page; with `loop` on it continues from the first.
- Known gap: WCAG 2.2.2 (Level A) asks auto-advancing content that runs longer than 5 seconds
  for a pause mechanism a reading user can find; stopping at the first interaction is the owner's
  chosen mechanism (§ 12, Q5). With `loop` off, a short sequence keeps the motion bounded.

### Reduced motion

Reduce Motion turns programmatic scrolls into jumps — `motion.css` zeroes every
`--component-*` and `--transforming-*` token, so a token-driven transition needs no override of
its own. Autoplay is off under Reduce Motion (above).

---

## 6. Color tokens

The container, the track and the item have no colour of their own — the content brings it. Only
the page indicator is coloured — and, in `overlay`, the pill under it.

| Element | Token | Notes |
|---|---|---|
| Container / Track / Item | — | Transparent |
| Dot — current | `var(--text-and-icon-primary)` | Candidate (§ 12, Q4) |
| Dot — other | `var(--surface-contrast-overlay)` | `Surface/ContrastOverlay` — Transparent/Black 20% in Day, Transparent/White 24% in Night; the alpha is baked into the token, no layer opacity (owner, 2026-11-01 — replaces `TextAndIcon/Secondary`, and the playground's 45 % opacity before it) |
| Indicator pill — `overlay` only | `var(--surface-dark-overlay)` | `Surface/DarkOverlay` — Transparent/Black 20% in Day and Night; over it the current dot is `var(--text-and-icon-always-light)` and the others `var(--surface-white-overlay)` — theme-fixed, so the dots read over a page's image, dark banners first (the DS pick, 2026-11-12; was a White 16% pill with the Below colours, which all but vanished on a dark banner). `below` has no pill |
| Item focus ring | the item content's own | The container draws no focus ring |

---

## 7. Typography

CarouselContainer has no text of its own. The items' text belongs to their content; the section
title above the row belongs to SectionHeader.

---

## 8. Spacing

| Property | Token | Value @100% | Notes |
|---|---|---|---|
| Edge inset (start of the first item, end of the last) | `var(--sp-s20)` | 20 | `edgeInset` default — the Figma component `[CarouselContainer] 3.0` (owner, 2026-11-02) |
| Container padding — top / bottom | `var(--sp-s8)` | 8 | Around the row (and, in `below`, the dots) |
| Gap between items — every mode | `var(--sp-s8)` | 8 | `gap` default; also between pages in Paged (§ 12, Q13) |
| Gap between items — dense | `var(--sp-s4)` | 4 | `gap: s4` |
| Peek | `var(--sp-s28)` | 28 | The visible part of the neighbour |
| Shelf item width (default) | `var(--sp-100-s96)` | 96 | Fixed in every SP mode, as is `itemWidth` (72–200) — the artwork too; only the text grows (§ 12 Q7, owner 2026-11-12) |
| Indicator padding | `var(--sp-s4)` | 4 | Around the dots, both placements — the pill's inset in `overlay` |
| `below` — row ↔ indicator | `var(--sp-s4)` | 4 | The bottom slot's top padding; row ↔ dots = 8 with the indicator padding |
| `below` — indicator ↔ container padding | `var(--sp-s8)` | 8 | The bottom slot's bottom padding |
| `overlay` — indicator ↔ row's bottom edge | `var(--sp-s8)` | 8 | The pill's bottom over the page's bottom edge |
| `overlay` — pill radius | `var(--sp-s28)` | 28 | A pill at every height |
| Indicator ↔ edge inset — `start` / `end` | `var(--sp-s12)` | 12 | The bottom slot's side padding; from the container edge `edgeInset + s12` = 32 |
| Dot | `var(--sp-130-s6)` | 6 × 6 | Scaling `cap130` — see below |
| Dot — current | `var(--sp-130-s16)` × `var(--sp-130-s6)` | 16 × 6 | Scaling `cap130` |
| Dot ↔ dot | `var(--sp-130-s4)` | 4 | Scaling `cap130` |
| Dot radius | half its height | 3 | A pill |

**Page indicator scaling — `cap130`** (owner, 2026-11-01). The dots and the gaps between them grow
with the SP mode up to 130% and hold the 130% size at the 150% step — the scaling mode of Heading 2 /
3 (`typography-rules.md` § 7): 6 / 16 / 4 at 100%, 7.8 / 20.8 / 5.2 at 130% and at 150%. The indicator
padding, the offsets around it and every other offset scale fully. Web: the `--sp-130-sN` family
(`SP/130% ratio` in Figma), which holds the 130% value at the 150% step.

---

## 9. Usage context

### When to use

- **Peek** — promo banners and featured offers on the home screen: one banner per view, the next
  one peeking in.
- **Peek, 2 per view** — paired cards (cities with a photo and a price).
- **Paged** — a short sequence of full-width slides, up to 7, where the position matters (dots).
- **Shelf** — category tiles, rides, intercity routes: many small items, scanned rather than read.

### When not to use

- For more than 7 pages — the dots stop being countable; use Shelf or a link to a list.
- For content the user has to compare side by side — a vertical list or a grid shows it at once.
- For the only path to an action — items off-screen are easy to miss; key actions stay visible.
- To page the content inside one card — that is a different pattern.
- Peek and dots together — the row shows one indicator.
- `overlay` over a page whose bottom centre — or the start / end corner it is aligned to — holds text or a button; the pill covers it; use `below` or another alignment.

### Related components

- [Card](https://super-dollop-pzmo65r.pages.github.io/card.md), [ServiceCard](https://super-dollop-pzmo65r.pages.github.io/service-card.md) — typical item content.
- [Grid](https://super-dollop-pzmo65r.pages.github.io/grid.md) — when every item is shown at once.
- [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) — the items' loading state.

---

## 10. Accessibility

Brief summary. Full spec: [`carousel-container-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/carousel-container.md).

- The row is named by its section title ("Promotions") — on iOS and Android through the
  SectionHeader heading above it, on the web as the region's label. The word "carousel" stays
  out of the label.
- Each item is one focus stop with its position: "{title}, {CTA}, button, 2 of 5". Moving focus to
  an item scrolls it into view. In an item with more than one control, the controls are focused
  one by one and the first carries the position.
- Every item is reachable without a swipe — by its own control, or by a focusable track when the
  items hold nothing focusable; Previous / Next buttons are open (§ 12, Q11).
- The dots are decorative: hidden from assistive technologies and not tappable (owner,
  2026-11-01). The item announces the position.
- Autoplay has no pause control: it stops at the first interaction and is off while an assistive
  technology or Reduce Motion is on (owner, 2026-11-01; WCAG 2.2.2 gap recorded in § 12, Q5).

---

## 11. Analytics and coverage contract

Contract per [`prototype-analytics-and-coverage.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/prototype-analytics-and-coverage.md).

| Field | Value |
|---|---|
| `data-ds-component` | `carousel-container` |
| Coverage unit | yes — the container; each item's content is its own coverage unit |
| Tap target model | none — the dots are decorative; the items' taps belong to their content |
| Actions | none |
| Internal targets | none |
| Emits value | no |

Attribute model: `data-ds-preset` = the mode (`peek` · `paged` · `shelf`), `data-ds-state` =
`standard`, `autoplay-running` or `autoplay-stopped`.

```html
<section data-ds-component="carousel-container" data-ds-component-id="home.promo.carousel"
         data-ds-preset="paged" data-ds-state="standard"
         aria-roledescription="carousel" aria-label="Promotions">
  <div class="carousel-container__track">
    <div class="carousel-container__item" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <!-- any DS component, with its own data-ds-component -->
    </div>
    …
  </div>
  <!-- pageIndicator: overlay adds carousel-container__dots--overlay;
       pageIndicatorAlign: start / end adds carousel-container__dots--start / --end -->
  <div class="carousel-container__dots" aria-hidden="true">
    <span class="carousel-container__dot carousel-container__dot--current"></span>
    <span class="carousel-container__dot"></span>
    …
  </div>
</section>
```

The classes are the web build's (`shared.css`). Instance props on the root: `--cc-edge`, `--cc-gap`, `--cc-peek`, `--cc-per-view`, `--cc-item-max`, `--cc-shelf-width`.

---

## 12. Open questions

Raised while moving the playground into the DS (2026-11-01).

### Resolved — owner, 2026-11-01 (Q2 revised 2026-11-02; Q6, Q7, Q11–Q13 on 2026-11-12)

| # | Question | Ruling |
|---|---|---|
| Q1 | Motion token for a programmatic scroll | moveM — `var(--component-move-m-appear)`, 400 ms, standard curve |
| Q2 | Edge inset | `s20` with `s8` above and below the row, as the Figma component `[CarouselContainer] 3.0` has it (owner, 2026-11-02 — supersedes the playground's `s12`); Peek then peeks `s28`, items 304 / Paged pages 320 at W 360 |
| Q3 | Gap values | `s8` (default) · `s4`, as in the playground. The Figma drafts (6 / 8 / 12) follow the spec |
| Q4 | Colour of the inactive dot | `Surface/ContrastOverlay` (was `TextAndIcon/Secondary` at full strength earlier the same day). The current dot stays `TextAndIcon/Primary` as a candidate |
| Q5 | Autoplay pause control | No pause control. Autoplay stops at the first interaction with the component. Recorded gap: WCAG 2.2.2 (Level A) asks for a pause mechanism a reading user can find without touching the row |
| Q10 | Interactive or decorative dots | Decorative — hidden from assistive technologies, not tappable, no hit area. Paged has no single-pointer alternative to the swipe (Q11) |
| Q6 | One announcement model | The per-platform conventions stand (owner, 2026-11-12): web "carousel" + "slide N of M" (APG), Android "list, item N of M", iOS a list with "N of M" in the label. Shared is the content — the position and the row's name; the a11y spec's "What each platform says" table is the contract |
| Q7 | Values outside the SP scale | Fixed (owner, 2026-11-12): item widths — Shelf `itemWidth` (72–200, default `s96` → `var(--sp-100-s96)`) and `maxItemWidth` (320) — and the artwork in an item hold their size in every SP mode; only the text grows, by its own text-style tokens, and wraps or truncates by its component's contract |
| Q11 | Keyboard, switch and voice path | No Previous / Next buttons (owner, 2026-11-12). The web keyboard path (the track a stop — arrow keys, Home / End) and the screen readers' scroll actions stay the alternatives; Paged has no single-pointer alternative to the swipe — the WCAG 2.5.1 (Level A) gap is accepted and recorded |
| Q12 | Localisation | "carousel", "slide", "N of M" and "Page N of M" are localised strings — a requirement, not a choice (recorded 2026-11-12) |
| Q13 | Paged page spacing | `s8`, as the Figma component has it (owner, 2026-11-12): at rest the next page shows `edgeInset − gap` = 12 at the end edge, a peek beside the dots |

### Open

| # | Question | Playground | Figma draft / DS |
|---|---|---|---|
| Q8 | Figma component | — | Built: `[CarouselContainer] 3.0` (`37890:33399`) — `Type` swaps the Peek (`37939:1014`), Paged (`37890:34400`, `PageIndicator` variant `Below` · `Overlay` + `🔴BottomSlot` boolean; the PageIndicator set has `Background` off · on — on in `Overlay`) and Shelf (`37890:33260`, `RTL` variant) sets. Open: no `PageIndicator` alignment property — `🔴BottomSlot` always centres it, so `pageIndicatorAlign: start` / `end` has no Figma counterpart yet; the indicator's padding, the dot gap and the bottom slot bind `SP/100% ratio` (fixed) and the current dot's width is unbound, while the owner ruling scales the dots and their gap `cap130`; the PageIndicator inside `🔴BottomSlot` exposes 3 of the master's 9 dots, so a row of 4–7 pages cannot show its indicator; there is no `s4` dense gap and no 2-per-view Peek variant |
| Q9 | `initialIndex` | In the parameter table, not built in the playground | Built on the web (`data-start-index`, a jump with no animation on load); confirm the mobile parameter name |

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.2 | 2026-11-12 | The Overlay page indicator: a `Surface/DarkOverlay` pill with the current dot `TextAndIcon/AlwaysLight` and the others `Surface/WhiteOverlay` — theme-fixed, so the dots read over a page's image, dark banners first (the DS pick, the owner having no preference; was a White 16% pill with the Below colours, which all but vanished on a dark banner). Status Draft → Ready (owner). |
| 3.0.1 | 2026-11-12 | Owner rulings (§ 12): Q6 — the per-platform announcement conventions stand; Q7 — item widths (Shelf `itemWidth`, default `var(--sp-100-s96)`, and `maxItemWidth`) and the artwork hold their size in every SP mode, only the text grows by its own tokens; Q11 — no Previous / Next buttons, the WCAG 2.5.1 gap accepted; Q12 — the strings are localised; Q13 — Paged keeps `s8`, the 12 peek. The preview's demo artwork moved to fixed `--sp-100-*` sizes. Still open: the Overlay current-dot colour over dark content; Q8 (Figma) and Q9 (the mobile parameter name). |
| 3.0.0 | 2026-11-01 | Initial spec, from the *CarouselContainer 3.0 Playground* (owner, 2026-11-01) and the Figma component `[CarouselContainer] 3.0` (`37890:33399`, page `37827:3312`) — a `Type` instance swap over the Peek, Paged and Shelf sets, whose geometry the spec follows (owner, 2026-11-02). Three modes: **Peek** — item `W − edgeInset − gap − peek` (304 at W 360), one or two per view (148), `maxItemWidth` / `item.maxWidth` caps; **Paged** — pages `W − 2 · edgeInset` (320) spaced by `gap` (Q13), decorative dots, autoplay and loop; **Shelf** — fixed 72–200 width (`s96` default), free scroll, `stretchItems` off by default. Edge inset `s20` with `s8` above and below, gap `s8` / `s4`, peek `s28`; no item minimum height — the row is as tall as its tallest item. Snapping per mode, the last item aligned to the end; one indicator per row. **Page indicator** (owner, 2026-11-06, from the component's Paged `PageIndicator` variant): `pageIndicator` = `below` · `overlay` · `none` — `below` sits in a bottom slot (`s4` above, `s8` below, the dots padded `s4`: 8 from the row), `overlay` lies over the page's bottom edge on a `Surface/WhiteOverlay` pill (radius `s28`, `s8` above the edge) and reserves no height; `pageIndicatorAlign` = `center` · `start` · `end`, `s12` in from the edge inset, mirrored in RTL (not yet a Figma property, Q8). Dots: current `TextAndIcon/Primary`, others `Surface/ContrastOverlay`, scaling `cap130` (`--sp-130-sN`). Autoplay off by default, stopped by the first interaction — one during an advance included — with no pause control (WCAG 2.2.2 gap recorded), never started under Reduce Motion. RTL from the locale; one focus stop per item with its position; items with nothing focusable make the track the stop (named, arrow keys / Home / End); a single item drops the carousel and slide roles. Owner rulings (2026-11-01): moveM for the programmatic scroll; decorative dots on by default; Shelf does not stretch; no pause control. Seven questions stay open (§ 12: Q6, Q7, Q8, Q9, Q11, Q12, Q13). Web build: `shared.css` `.carousel-container`, `DS.initCarousel`, the preview page (7 sections), `capabilities.json`, the registry row; bound in `manifest/web-nodes.json` with empty `nodes` (the Swoosh precedent) — neither platform bridge carries the component yet. |

---

# CarouselContainer — Accessibility

The container is a named row; each item is one stop that says where it is in the row. A screen
reader user moves item by item, and moving focus scrolls the row — the user never has to swipe the
row itself to reach an item. A keyboard, switch or voice user has a way to move the row too, even
when the items hold nothing focusable (§ Keyboard and switch access).

Label rule: the container is named by its section ("Promotions"), never "Carousel" or "Promotions
carousel" — the platform's role adds that word where the platform has one. On iOS and Android an
item is named by its content — ✅ "20% off your first ride, Learn more, button, 2 of 5", ❌
"Slide 2". The web names the item by its position and lets the content follow (APG, § Web).

The phrases below are indicative: the exact order, and hints such as "Double-tap to activate",
vary with the screen-reader version and verbosity settings. The position ("N of M"), the role
descriptions and "Page N of M" are localised strings.

## What each platform says

| | Entering the row | On an item |
|---|---|---|
| **Android · TalkBack** | The SectionHeader heading ("Promotions, heading"), then "in list, 5 items" | "20% off your first ride, Learn more, button, item 2 of 5" |
| **iOS · VoiceOver** | The SectionHeader heading, then "list start" | "20% off your first ride, Learn more, 2 of 5, button" |
| **Web (APG)** | "Promotions, carousel" | "2 of 5, slide, 20% off your first ride, Learn more, link" |

The three differ by platform convention, and the conventions stand (owner, 2026-11-12 — spec § 12, Q6):
shared is the content — the position and the row's name.

## Android · TalkBack

Jetpack Compose.

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Section name | The SectionHeader above the row, `heading()` — the row's name comes from it | — | Heading | — |
| Container | Optional `semantics { contentDescription = "Promotions" }`; a non-focusable container's description is not reliably spoken on entry, so the heading carries the name | — | A list: `CollectionInfo(rowCount = 1, columnCount = N)` — "in list, 5 items" | — |
| Item — one action | The content's own phrase, one merged node (`mergeDescendants = true`): "{title}, {CTA}" | — | Button; `CollectionItemInfo` gives "item 2 of 5" | "Double-tap to activate" (system) |
| Item — several controls | Not a focus stop of its own (`mergeDescendants = false`): each control is focused separately. The first control carries the position: "Learn more, item 2 of 5" | — | The controls' own | — |
| Item — non-interactive | The content's text, one merged node | — | Text, with "item 2 of 5" | — |
| Dots | — | — | Decorative: `clearAndSetSemantics {}`, not tappable (owner, 2026-11-01) | — |

- Swiping right moves focus to the next item and scrolls the row to it.
- `ACTION_SCROLL_FORWARD` / `BACKWARD` page the row in Paged and move one item in Peek. TalkBack
  speaks its own scroll feedback ("showing items 2 to 3 of 5"), not "Page 2 of 5" — the asymmetry
  with iOS is accepted.
- In RTL, `ACTION_SCROLL_FORWARD` moves toward the visual left.

### Edge states

- **Zero items:** the row is removed with its SectionHeader; no empty list is announced.
- **Single item:** no list semantics — the item is read on its own, without "1 of 1".
- **Loading:** the skeleton items are one node — "Loading" — or `invisibleToUser`; the list is
  announced once the items arrive, without moving focus.
- **Items change while focused:** focus stays on the same item (by key); a removed focused item
  moves focus to its neighbour.
- **Error:** an item's error belongs to its content. A row that fails as a whole is removed (as zero
  items).
- **Disabled:** not applicable — the container has no disabled state; a disabled item content
  announces its own "disabled".
- **Autoplay:** does not run while TalkBack, Switch Access or Voice Access is on; stops if one of
  them starts mid-run, and at the first interaction. There is no pause control (owner, 2026-11-01)
  — see § Known gap.
- **RTL:** the first item is on the right; "item N of M" follows the reading order; the dots run
  right to left and the current dot elongates in the reading direction.
- **Font scaling (200 %):** items grow in height and the row stretches to the tallest; item width
  does not follow the font — content wraps inside the item.

---

## iOS · VoiceOver

UIKit names; the SwiftUI equivalents are noted.

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Section name | The SectionHeader above the row | — | `.header` | — |
| Container | — (the heading names the row) | — | `accessibilityContainerType = .list` — "list start" / "list end" (SwiftUI: the row as `.accessibilityElement(children: .contain)`) | — |
| Item — one action | "{title}, {CTA}, 2 of 5" — one element; the position is a localised suffix of the label (SwiftUI: `.accessibilityElement(children: .combine)` + `.accessibilityLabel`) | — | `.button`, set explicitly — combining does not add it | — |
| Item — several controls | Each control is its own element; the first control's label carries the position ("Learn more, 2 of 5") | — | The controls' own | — |
| Item — non-interactive | "{content}, 2 of 5" | — | `.staticText` | — |
| Dots | — | — | Decorative: `accessibilityHidden(true)`, not tappable | — |

- Swiping right moves focus to the next item and scrolls the row to it.
- A three-finger swipe pages the row: `accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool`
  scrolls, and the app posts the localised "Page 2 of 5" via `UIAccessibility.post(notification:
  .pageScrolled, argument:)` (SwiftUI: `accessibilityScrollAction`). The phrase is app-supplied, not
  produced by the platform.
- The scroll direction is physical (`.left` / `.right`): in RTL "next" is `.right`.
- No public iOS role description says "carousel"; the row is a list named by its heading.

### Edge states

- **Zero items / Loading / Items change while focused / Error / RTL:** as on Android.
- **Single item:** read as a plain element, without "1 of 1"; no three-finger paging.
- **Autoplay:** does not run while VoiceOver, Switch Control or Voice Control is on; stops if one
  of them starts mid-run.
- **Dimmed:** not applicable — the container is never dimmed; a dimmed item content announces its
  own "dimmed".
- **Dynamic Type:** as Android's font scaling — the row grows in height, widths stay.

---

## Keyboard and switch access

Reaching every item without a swipe — WCAG 2.1.1. Today this holds only when every item holds a
focusable control:

- **Items with a control** — Tab reaches each one; focusing it scrolls it to its snap point.
- **Items with nothing focusable** (a banner without a link) — the track itself is the stop: web
  `tabindex="0"` with a name and arrow-key scrolling; native a focusable row with scroll actions.
- **No Previous / Next buttons** (owner, 2026-11-12 — spec § 12, Q11): the keyboard path and the
  screen readers' scroll actions are the alternatives.
- **Peeking neighbour:** a partly visible item is in the focus order; focusing it scrolls it fully in.

Dragging (WCAG 2.5.7): scrolling is the platform's own scroll view, with no custom drag handler,
so the swipe is a user-agent gesture. The dots are decorative, so no mode has a single-pointer
alternative — the WCAG 2.5.1 (Level A) gap is accepted by the owner (2026-11-12, spec § 12, Q11).

---

## Web preview (reference)

Follows the [APG carousel pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/) (grouped,
no tabs):

- **Container:** `<section aria-roledescription="carousel" aria-label="Promotions">` — a named
  region; "carousel" and "slide" are localised.
- **Item:** `role="group" aria-roledescription="slide" aria-label="2 of 5"` — the content inside
  follows. Off-screen items stay in the tab order; focusing one scrolls it into view
  (`scroll-padding-inline` = the edge inset). A single item drops the carousel and slide roles.
- **Dots:** decorative — `<span>`s in a container with `aria-hidden="true"`, no buttons, no
  pointer handler (owner, 2026-11-01).
- **Focusable track:** when no item holds a focusable element, `DS.initCarousel` makes the track
  the stop — `tabindex="0"`, named after the row (its `aria-label`), with a focus ring; Left / Right
  move it item by item (mirrored in RTL), Home / End to the first / last item.
- **Autoplay:** pauses while the pointer hovers the row (`pointerenter` / `pointerleave`), and stops
  for good on `pointerdown`, `wheel`, `touchstart`, `keydown` or `focusin` — one that arrives during
  an advance stops it too and cancels the animation; it never starts under
  `prefers-reduced-motion: reduce`. The row only scrolls — the slides never leave the DOM — so there
  is no live region and nothing is announced on a change. No pause button (owner, 2026-11-01).
- **Assistive technologies:** a browser cannot detect a screen reader, so on the web autoplay keeps
  running under one until an interaction stops it — see § Known gap.

### Colour and contrast

| Pairing | Tokens | Measured |
|---|---|---|
| Current dot on the page | `--text-and-icon-primary` on `--background-primary` | ≈ 17.8:1 Day / ≈ 17.0:1 Night (measured for the Swoosh, `swoosh-a11y.md`) — candidate (spec § 12, Q4) |
| Other dots | `--surface-contrast-overlay` (owner, 2026-11-01) — Black 20% Day / White 24% Night | ≈ 1.6:1 Day on `Background/Primary`, ≈ 2.2:1 Night on `Background/Primary` (Grey 875). The dots are decorative, so WCAG 1.4.11 sets no floor; the current dot stays distinct by its `TextAndIcon/Primary` fill and its width |
| Dots in `overlay` | Current `--text-and-icon-always-light`, others `--surface-white-overlay`, on a `--surface-dark-overlay` pill (Black 20%) over the page's content — theme-fixed (2026-11-12) | Set by the image under the pill, not by `Background/Primary` — the dark pill lifts the white current dot off dark banners and still carries it on light ones. Decorative, so WCAG 1.4.11 sets no floor; `overlay` belongs on pages whose bottom centre is calm enough for the current dot to read |
| Current vs other dot | — | The current dot also differs in shape (16 × 6 vs 6 × 6) — colour is not the only signal |

---

## Known gap — autoplay without a pause control

WCAG 2.2.2 Pause, Stop, Hide (Level A) asks auto-advancing content that starts on its own, runs
longer than 5 seconds and sits beside other content for a mechanism to pause it. The owner's
ruling (2026-11-01) is no pause control: autoplay stops at the first interaction with the
component and is off under Reduce Motion, and on iOS / Android under an assistive technology. A user who reads without
touching the row — low vision with magnification, a cognitive disability — has no visible way to
stop it. Mitigations in the spec: off by default, Paged only, `loop` off by default (the motion ends
on the last page), interval 7 s by default.

**Web: screen-reader browse mode.** The web cannot tell that a screen reader is running, and in NVDA /
JAWS browse mode the virtual cursor fires none of the stop events (no focus, key or pointer event
reaches the row), so the row keeps advancing under a reading user.

---

## Machine contract — `specs/components/carousel-container/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "carousel-container",
  "name": "CarouselContainer",
  "version": "3.0.2",
  "description": "A horizontally scrolling row of DS components in three modes — Peek (one or two items per view, the next peeking in by s28), Paged (one item per page, decorative dots below the row or over it, optional autoplay) and Shelf (fixed-width items, free scroll). The item width is computed from the container width; edge inset s20 (s8 above and below), gap s8 / s4. Owns the row only — each item's content slot takes any DS component.",
  "files": {
    "spec": "specs/components/carousel-container/carousel-container.md",
    "a11y": "specs/components/carousel-container/carousel-container-a11y.md",
    "preview": "src/carousel-container.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "[CarouselContainer] 3.0 — new component file",
    "fileKey": "7zltjwJiJP7P9qYPoeTGiW",
    "componentSets": {
      "[CarouselContainer] 3.0": {
        "key": "",
        "note": "Component 37890:33399 on page 37827:3312, Type instance-swap over the Peek (37939:1014), Paged (37890:34400, PageIndicator variant Below|Overlay + 🔴BottomSlot boolean) and Shelf (37890:33260, RTL off|on) sets. Geometry follows it (owner, 2026-11-02): edge inset s20, s8 above and below, gap s8, peek s28."
      }
    },
    "capturedAt": "2026-11-06"
  },
  "root": {
    "class": "carousel-container",
    "dataDsComponent": "carousel-container"
  },
  "anatomy": {
    "track": {
      "class": "carousel-container__track",
      "notes": "The scrolling row: padding-inline = --cc-edge (the edge inset, also the snap point), gap --cc-gap, scroll-snap per mode. No fill, border or radius."
    },
    "item": {
      "class": "carousel-container__item",
      "notes": "One CarouselItem: width per mode from the container width, height by content; the row is as tall as its tallest item. One content slot — any DS component, which brings its own shape and colour."
    },
    "dots": {
      "class": "carousel-container__dots",
      "optional": true,
      "notes": "The page indicator, Paged only (pageIndicator: none omits the element). Decorative: aria-hidden, not tappable. Padded s4; below: s4 under the row and s8 under it to the container padding; overlay (.carousel-container__dots--overlay): over the page's bottom edge, s8 above it, on a Surface/WhiteOverlay pill (radius s28), no height reserved. Dots s6, the current one s16 × s6, s4 apart; dots scale cap130 (--sp-130-sN).",
      "onlyWhen": {
        "mode": [
          "paged"
        ]
      }
    },
    "dot": {
      "class": "carousel-container__dot",
      "notes": "One per item; .carousel-container__dot--current marks the current page (TextAndIcon/Primary), the others Surface/ContrastOverlay."
    }
  },
  "axes": {
    "mode": {
      "title": "Mode",
      "type": "enum",
      "values": [
        "peek",
        "paged",
        "shelf"
      ],
      "default": "peek",
      "css": {
        "mechanism": "peek = bare .carousel-container; paged = .carousel-container--paged; shelf = .carousel-container--shelf — mirrored in data-ds-preset"
      },
      "figma": {
        "kind": "component-set",
        "notes": "Instance swap Type#37890:18 picks the Peek / Paged / Shelf sets."
      },
      "constraints": [
        "One indicator per row: the peek (Peek), the dots (Paged) or the cut-off last item (Shelf) — never the peek and the dots together.",
        "Peek snaps to the item's start, one item per swipe; Paged snaps per page with snap-stop; Shelf scrolls freely."
      ]
    },
    "itemsPerView": {
      "title": "Items per view",
      "type": "enum",
      "values": [
        "1",
        "2"
      ],
      "default": "1",
      "css": {
        "customProperty": "--cc-per-view"
      },
      "figma": {
        "kind": "none"
      },
      "constraints": [
        "Peek only. Item width = (W − edge inset − gap − peek − (N − 1) · gap) / N: 304 at N 1, 148 at N 2 (W 360)."
      ]
    },
    "edgeInset": {
      "title": "Edge inset",
      "type": "token",
      "tokens": [
        "--sp-s20"
      ],
      "default": "--sp-s20",
      "css": {
        "customProperty": "--cc-edge"
      },
      "figma": {
        "kind": "none",
        "notes": "Owner, 2026-11-02: s20, as the Figma component [CarouselContainer] 3.0 has it (supersedes the playground's s12); the container also pads s8 top and bottom."
      },
      "constraints": [
        "The container also pads s8 top and bottom (the Figma component)."
      ]
    },
    "gap": {
      "title": "Gap",
      "type": "token",
      "tokens": [
        "--sp-s8",
        "--sp-s4"
      ],
      "default": "--sp-s8",
      "css": {
        "customProperty": "--cc-gap"
      },
      "figma": {
        "kind": "none"
      },
      "constraints": [
        "Every mode, between Paged pages too: at rest the next page shows edge inset − gap (12) — open question Q13."
      ]
    },
    "peek": {
      "title": "Peek",
      "type": "token",
      "tokens": [
        "--sp-s28"
      ],
      "default": "--sp-s28",
      "css": {
        "customProperty": "--cc-peek"
      },
      "figma": {
        "kind": "none"
      }
    },
    "maxItemWidth": {
      "title": "Max item width",
      "type": "number",
      "min": 0,
      "max": 600,
      "customizable": "a dp cap, 0 = none (e.g. 320 = calc(var(--sp-s160) * 2)); Peek only — the neighbour then peeks in by W − edge inset − max − gap",
      "default": 0,
      "css": {
        "customProperty": "--cc-item-max"
      },
      "figma": {
        "kind": "none"
      }
    },
    "itemWidth": {
      "title": "Shelf item width",
      "type": "token",
      "tokens": [
        "--sp-100-s96"
      ],
      "customizable": "72–200 dp; Shelf only — fixed in every SP mode (owner, 2026-11-12)",
      "default": "--sp-100-s96",
      "css": {
        "customProperty": "--cc-shelf-width"
      },
      "figma": {
        "kind": "none"
      }
    },
    "stretchItems": {
      "title": "Stretch items",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": ".carousel-container--stretch — Shelf items that fit share the row; once they overflow, the item width applies"
      },
      "figma": {
        "kind": "none",
        "notes": "Owner, 2026-11-01: Shelf does not stretch by default."
      }
    },
    "pageIndicator": {
      "title": "Page indicator",
      "type": "enum",
      "values": [
        "below",
        "overlay",
        "none"
      ],
      "default": "below",
      "css": {
        "mechanism": "below = .carousel-container__dots; overlay = .carousel-container__dots.carousel-container__dots--overlay; none = no dots element (Paged only)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "PageIndicator",
        "notes": "Paged set: PageIndicator Below|Overlay; none = the 🔴BottomSlot boolean off. Owner, 2026-11-06 — replaces the boolean."
      },
      "constraints": [
        "Paged only.",
        "overlay covers the page's bottom centre — not for pages with text or a button there."
      ]
    },
    "pageIndicatorAlign": {
      "title": "Page indicator alignment",
      "type": "enum",
      "values": [
        "center",
        "start",
        "end"
      ],
      "default": "center",
      "css": {
        "mechanism": "center = no modifier; start = .carousel-container__dots--start; end = .carousel-container__dots--end — logical, mirrored by dir=\"rtl\""
      },
      "figma": {
        "kind": "none",
        "notes": "Owner, 2026-11-06. 🔴BottomSlot always centres the indicator — no Figma property yet (open question Q8)."
      },
      "constraints": [
        "Paged only, with pageIndicator below or overlay.",
        "start / end sit s12 in from the edge inset (32 from the container edge at s20)."
      ]
    },
    "initialIndex": {
      "title": "Initial index",
      "type": "number",
      "min": 0,
      "max": 99,
      "step": 1,
      "default": 0,
      "css": {
        "mechanism": "data-start-index on the root — DS.initCarousel opens on that item with a jump"
      },
      "figma": {
        "kind": "none"
      }
    },
    "autoplay": {
      "title": "Autoplay",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "data-autoplay=\"<ms>\" (3000–10000, 7000 by default) + data-loop on a Paged root — DS.initCarousel advances on moveM"
      },
      "figma": {
        "kind": "none"
      },
      "constraints": [
        "Paged only. Stops at the first interaction (touch, swipe, wheel, key, focus — during an advance too) and does not resume; pauses on hover (web).",
        "Off under Reduce Motion, and on iOS / Android under an assistive technology; the web cannot detect a screen reader (a11y Known gap).",
        "No pause control (owner, 2026-11-01) — a recorded WCAG 2.2.2 gap."
      ]
    },
    "autoplayInterval": {
      "title": "Autoplay interval",
      "type": "number",
      "min": 3000,
      "max": 10000,
      "step": 500,
      "default": 7000,
      "css": {
        "mechanism": "the value of data-autoplay=\"<ms>\" on a Paged root"
      },
      "figma": {
        "kind": "none"
      },
      "constraints": [
        "Paged with autoplay only. How long one page shows, in ms."
      ]
    },
    "loop": {
      "title": "Loop",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "data-loop on a Paged root — after the last page, the first"
      },
      "figma": {
        "kind": "none"
      },
      "constraints": [
        "Paged with autoplay only. Off: autoplay stops on the last page."
      ]
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "dir=\"rtl\" from the locale: the first item on the right, the track and the dots mirrored"
      },
      "figma": {
        "kind": "none"
      }
    },
    "accessibilityLabel": {
      "title": "Accessibility label",
      "type": "text",
      "default": "the section title",
      "css": {
        "mechanism": "aria-label on the root (and on the track when it is the focus stop) — without the word \"carousel\""
      },
      "figma": {
        "kind": "none"
      }
    }
  },
  "states": {
    "default": [
      "standard"
    ],
    "paged": [
      "standard",
      "autoplay-running",
      "autoplay-stopped"
    ]
  },
  "analytics": {
    "dataDsComponent": "carousel-container",
    "action": "none",
    "notes": "Coverage unit; each item's content is its own coverage unit with its own data-ds-component. No tap target — the dots are decorative. data-ds-preset = the mode, data-ds-state = standard / autoplay-running / autoplay-stopped."
  },
  "rtl": {
    "supported": true,
    "notes": "Direction follows the locale; no separate parameter. Each item's content follows its own RTL rules."
  },
  "constraints": [
    "The container, track and item paint nothing: no fill, border, radius or shadow; no Pressed, Disabled or Skeleton state of their own.",
    "Programmatic scrolls (autoplay, focus to an off-screen item) run on moveM — var(--component-move-m-appear); swipe, fling and snap are native physics.",
    "Dots: current TextAndIcon/Primary, others Surface/ContrastOverlay (owner, 2026-11-01), in below; in overlay a Surface/DarkOverlay pill with the current dot TextAndIcon/AlwaysLight and the others Surface/WhiteOverlay (theme-fixed, 2026-11-12); up to 7.",
    "One item: Peek / Paged at W − 2 · edge inset, Shelf at its own width; no scrolling, no indicator.",
    "Neither mobile platform ships it — no manifest node binding until a platform bridge carries [CarouselContainer] 3.0."
  ]
}
```
