> ## 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/progress-steps-list.html
> Source: specs/components/progress-steps-list/progress-steps-list.md, specs/components/progress-steps-list/progress-steps-list-a11y.md, specs/components/progress-steps-list/capabilities.json

---

# ProgressStepsList · Oymyakon DS 3

> A vertical list of the steps of a multi-step flow — each step a status squircle on a connecting
> line, with the step's description beside it — for the full, detailed breakdown of the flow.

**Version:** 3.0.2 · **Status:** Ready · **Figma:** `[ProgressStepList] 3.0` (file `uKPcsUAZ9fMW6H9KierbE9`, page `39122:7587`; master `38825:9975`, variant set `ProgressStepsList` `38898:243673` — `Skeleton = Off` `38825:9686` / `On` `38898:243704`; building blocks `ListStepItem` `38832:10703`, `🔴StartSlot` `38825:8274`, `🔵EndSlot` `38825:8330`, `DefaultItem` `38825:10067`, `StepItem` `38767:1381` — shared with ProgressStepsCompact)

---

## 1. Description

ProgressStepsList shows every step of an ordered flow with its details — a courier delivery, for
example: go to the sender → pick up → go to the recipient → drop off, each with an address, a
person and an order tag. Each step is a row: on the start side a small squircle (the step's number
or an icon) whose look follows the step's status — completed, current, upcoming, cancelled — with a
vertical line running down to the next step, so the list reads as one path; on the end side the
step's description.

The description is a slot. Out of the box it holds **DefaultItem** — a Cell with a title and a
subtitle — not tappable by default; when tapping is on, the whole cell is the target and shows a
chevron. When DefaultItem falls short (a step with a tag, an address, a courier and a photo), a team
builds its own layout from DS components and puts it in the slot.

The list is the full form of the flow. Its compact sibling,
[ProgressStepsCompact](https://super-dollop-pzmo65r.pages.github.io/progress-steps-compact.md), shows the first four
steps in a row and opens this list from its MoreButton.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | — | — | Not in the platform bridge (manifest seed, 2026-11-11) |
| Android | — | — | Not in the platform bridge (manifest seed, 2026-11-11) |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.progress-steps-list`) | 3.0.0 | Reference implementation, this repo; preview page `src/progress-steps-list.njk` — the first build of this component on any platform |

---

## 2. Anatomy

```
ProgressStepsList (.progress-steps-list)             fill width · column · no gap between steps
├── Phrase (.visually-hidden, role="status")        the spoken position — not drawn
└── Steps (ol.progress-steps-list__steps)
    └── ListStepItem (li.progress-steps-list__item)  row · s20 start/end · gap s8 · min 64
        ├── 🔴StartSlot (.progress-steps-list__start) column · centred · gap s4 · s4 at the bottom
        │   ├── StepItem (.squircle.squircle--s)     Squircle S, state from the step status
        │   └── Divider (.progress-steps-list__divider)  s2 wide · fills down to the next step · absent on the last step
        └── 🔵EndSlot (.progress-steps-list__end)     fill · s4 at the bottom · min s24
            └── DefaultItem (.cell.progress-steps-list__default)  or the team's own layout
```

| Part | Required | Description |
|---|---|---|
| **Container** | Required | A vertical stack of steps, full width, no gap between them — the Divider joins them |
| **ListStepItem** | Required, one per step | A row: StartSlot and EndSlot, `s8` apart, padding `s20` on the start and end sides, none on top and bottom; at least 64 high, and as tall as its EndSlot content |
| **StepItem** | Required | A [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) S (`s40` box, radius `s16`) — the same building block as ProgressStepsCompact (`38767:1381`). Its State follows the step status (§ 4); the content is the step's number (Text type) or an icon (Icon type) |
| **Divider** | On every step but the last | A vertical line, `s2` wide, radius `s8`, `Border/Default`, centred under the squircle, `s4` below it; it fills the row's remaining height and stops `s4` above the next step's squircle. The last step has none — the path ends there |
| **EndSlot** | Required | The step's description. Fill width, `s4` padding at the bottom, at least `s24` high. Holds DefaultItem or a team's own content (§ 3) |
| **DefaultItem** | Default content of EndSlot | A [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md) (`[Cell] 3.4` / `[CustomCell] 3.3` in Figma) with a title (Main Body, `TextAndIcon/Primary`) and an optional subtitle (Compact Body, `TextAndIcon/Secondary`); no padding of its own, its text block centred on the squircle (`s40` high). Not tappable by default; tappable, it shows the `cell-chevron` in its end slot (§ 3) |

- The squircles and Dividers are drawn only: the steps' positions and statuses are spoken in words
  (§ 10).
- The Divider's colour does not carry the status — it is the same static line between every pair
  of steps.

### RTL layout

The Figma set stages `RTL = On` for ListStepItem: StartSlot moves to the right edge, EndSlot to its
left, the EndSlot content aligns to the right. The step content does not mirror — numbers and
icons read the same either way; the DefaultItem chevron mirrors (it is the Cell's
`cell-chevron`, `rtl: true`). An icon marked RTL in the icon library (`courier-walk`) mirrors, as on
ProgressStepsCompact.

On the web: `dir="rtl"` on the root or an ancestor reverses each row; the chevron mirrors through
the Cell's own `:dir(rtl)` rule, and an RTL-marked step icon takes
`.progress-steps-list__icon--rtl` (`scaleX(-1)` under `:dir(rtl)`).

---

## 3. Variants and sizes

One size. Figma axes: `Skeleton` on the set, `RTL` × `Skeleton` on ListStepItem. The other rows
are slot choices and nested-component types.

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Steps** | 2 and up | — | One `li.progress-steps-list__item` per step, in flow order. No upper limit: the list grows with the flow and the host scrolls it |
| **Skeleton** | `off` · `on` | `off` | `.progress-steps-list--skeleton` + four placeholder rows (§ 4) |
| **RTL** | `off` · `on` | `off` | `dir="rtl"` on the root or an ancestor |
| **Step content** | `text` · `icon` | `text` | The Squircle's own Type: `.squircle--text` (a number) or the bare `.squircle--s` with a 24 icon in the slot |
| **EndSlot content** | `default` · custom | `default` | DefaultItem, or the team's own DS layout inside `.progress-steps-list__end` |
| **Tappable** (DefaultItem) | `off` · `on` | `off` | Off: a `div.cell` with no end slot. On: a `button.cell` with the `cell-chevron` in `.cell-end-slot` |
| **Subtitle** (DefaultItem) | `on` · `off` | `on` | The second `.text-row` present or absent |

### EndSlot — DefaultItem or the team's own content

- **DefaultItem** — the standard description: a title (the step's name) and a subtitle. It is a
  Cell with its padding and floor removed, so its text block sits centred on the 40 squircle.
  Tapping is off by default — the step is information. A host that opens a step's details turns
  it on: the whole cell becomes the target and shows the chevron, which is how the row says it
  can be opened (owner, 2026-11-11). The press is the Cell's own (`pushHighlight`, § 5).
- **The team's own content** — when DefaultItem falls short, a team composes the description from
  any DS components and puts it in the slot — `StepItemCouriers` in the Figma example: a Heading 3
  name with an order [Tag](https://super-dollop-pzmo65r.pages.github.io/tag.md), the address, a mini-avatar with the courier's name, a
  photo of the parcel and a chevron, `s24` at its bottom. The list gives the slot only: hit
  targets, tap areas, states and the bottom spacing are the content's own, set inside the team's
  component.

### Step content — text or icon

A step's squircle holds the step's **number** (the Squircle's Text type) or an **icon** (the
Squircle's Icon type) — one content type per list. The icon is a 24 glyph from `icons/outlined/`;
it takes the squircle's content colour, so the status colours it as it colours a number (§ 6).

Example — the courier flow of the Figma screen:

| Step | Icon (`icons/outlined/`) | Meaning |
|---|---|---|
| Go to sender | `profession/person-courier` | Walk to the sender |
| Pick up | `actions/info-outline` | Collect the parcel |
| Go to recipient | `profession/courier-walk` | Walk to the recipient — mirrors in RTL |
| Drop off | `profession/courier` | Hand over the parcel |

- Web: the bare `.squircle.squircle--s` (no `.squircle--text`) with the inline SVG in
  `.squircle__slot`, `fill="currentColor"`, sized `var(--sp-s24)`.

### Custom base

The custom base is the main preset's structure — the steps, the Divider, the two slots, the spoken
phrase — with these parts re-pointed by a team:

| What | Custom base | Web hook (unset = the main preset's value) |
|---|---|---|
| StepItem | The Squircle swapped for another DS component — an [IconContainer](https://super-dollop-pzmo65r.pages.github.io/icon-container.md), for example | The element keeps `.progress-steps-list__step`; an IconContainer step adds `.progress-steps-list__step--icon-container` (24 glyph, `var(--sp-s24)`) and takes its status colour through its own `color` |
| Status colours (Squircle steps) | The fill and content of each status | `--progress-steps-list-{completed · current · upcoming · cancelled}-fill` / `-content` — unset, each is the Squircle state's own value (§ 6) |
| Divider | Colour and width | `--progress-steps-list-divider` (`var(--border-default)`) · `--progress-steps-list-divider-width` (`var(--sp-s2)`) |
| Paddings | Any SP value | `--progress-steps-list-padding-inline` (`var(--sp-100-s20)`) · `--progress-steps-list-gap` (`var(--sp-100-s8)`, StartSlot to EndSlot) |
| EndSlot content | Any DS layout (§ 3, EndSlot) | The team's markup inside `.progress-steps-list__end` |
| Skeleton shape | Placeholders in the form of the team's own steps | Any DS Skeleton shapes inside `.progress-steps-list__end` of the skeleton rows |

- An instance on the custom base carries `data-ds-preset="custom"` (§ 11).
- A re-pointed colour is the team's contrast check (`color-rules.md` § contrast). A swapped
  StepItem shows the status in more than colour where the status matters; the spoken phrase is
  unchanged (§ 10).

---

## 4. States

### Component

| State | Description |
|---|---|
| **Standard** | The list with its steps; each step shows its status |
| **Skeleton** | Loading: the flow's steps are not known yet. Four placeholder rows — a Squircle S in its own Skeleton state, a Divider under the first three, and two text bars in EndSlot (full width, 16 high; 120 wide, 12 high; radius `s8`, `s4` apart, `s4` above and below) — Figma `Skeleton = On` (`38898:243704`) |
| **Disabled** | The whole list unavailable. Every StepItem takes the Squircle's own Disabled state in place of its status; the Dividers stay unchanged; the EndSlot content takes its own Disabled state — DefaultItem the Cell's Disabled (owner, 2026-11-11 — not staged in Figma) |

ProgressStepsList has no Error state of its own — a cancelled step is a step status, not a list
state.

- **Skeleton draws four placeholder rows**, whatever the flow's length — the length is not known
  while loading. The shape is static in the main preset; a Skeleton that matches the team's own
  steps is built on the custom base (§ 3).
- Web: the root takes `.progress-steps-list--skeleton` and `data-ds-state="skeleton"`; each step is
  `squircle squircle--s squircle--skeleton progress-steps-list__step` with no content; the bars are
  DS `.skeleton` shapes in `.progress-steps-list__placeholder`. The phrase reads "Loading"; the
  placeholder list is `aria-hidden` (§ 10).
- **Disabled** draws no status: the steps look alike, so the position is carried by the spoken
  phrase alone (§ 10). DefaultItem is the Cell's `.is-disabled` (its text in
  `TextAndIcon/Disabled`); a tappable DefaultItem is not tappable while the list is Disabled. The
  team's own content is responsible for its own Disabled state — the list re-points nothing inside it.
- Web: the root takes `.progress-steps-list--disabled` and `data-ds-state="disabled"`; each step
  squircle wears `.squircle--disabled` instead of its status class (the Cancelled class too);
  DefaultItem takes `.is-disabled`, and a tappable one carries `aria-disabled="true"` — not
  `disabled`, so it stays reachable (the NumericStepper precedent) — with its handler returning
  early while it is set.

### Step status (StepItem)

The StepItem is ProgressStepsCompact's — the same statuses and colours (Figma StepItem
description).

| Status | Squircle State | Fill | Content | Meaning |
|---|---|---|---|---|
| **Completed** | Disabled | `Surface/OnWhite` | `TextAndIcon/Disabled` | The step is done |
| **Current** | Inverse | `Background/InversePrimary` | `DoubleColor/Outline-Inverse` | The step in progress — one per list at most |
| **Upcoming** | Standard (Figma: Default) | `Surface/OnWhite` | `TextAndIcon/Primary` | The step is ahead |
| **Cancelled** | Standard (Figma: Default) | `Surface/OnWhite` | `TextAndIcon/Error` | The step did not happen |

- The status is drawn by colour alone; the EndSlot content keeps its own colours whatever the
  status. Assistive tech hears the status in words (§ 10).

### DefaultItem (tappable)

| State | Description |
|---|---|
| **Standard** | The cell at rest, the chevron at its end |
| **Pressed** | Held: the Cell's `var(--shadow-pressed)` overlay (`pushHighlight`, § 5) |
| **Focused** (web) | Keyboard focus: the Cell's inset focus ring |
| **Disabled** | With the list's Disabled state only: the Cell's Disabled colours, no press, no tap; reachable — `aria-disabled="true"` on the web |

---

## 5. Animation and behavior

Reference: [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md).

| Event | Pattern | Token | CSS |
|---|---|---|---|
| DefaultItem press (tappable) | `pushHighlight` | `var(--component-push-highlight)` | The Cell's own `box-shadow: var(--shadow-pressed)` |
| DefaultItem tap | — | — | Opens the step's details — the host's navigation; the component emits the tap. The Cell's tap debounce applies (Cell § 11.6) |
| Disabled | — | — | No press and no tap; Standard ↔ Disabled is a swap of the squircle and Cell states, no transition |
| Step status change | — | — | A swap: the host re-renders the step with its new Squircle State; no transition (Squircle declares none) |
| Skeleton | `Patterns/Shimmer` | `var(--pattern-shimmer)` | The Squircle Skeleton's and the Skeleton bars' own sweep; zeroed under reduced motion at the token |
| Skeleton → Standard | — | — | A swap: the host replaces the placeholder rows with the steps in one frame |
| Divider | — | — | Static — no motion |
| Team content | Its components' own motion | Their specs | ProgressStepsList adds nothing |

- Reduced motion: `motion.css` zeroes every `--component-*` and `--pattern-*` token inside
  `prefers-reduced-motion: reduce`; the component carries no override.

---

## 6. Color tokens

| Element | Token | Notes |
|---|---|---|
| Container | — | Transparent — the list sits on its host's surface |
| Step — Completed | `var(--surface-on-white)` · `var(--text-and-icon-disabled)` | `.squircle--disabled` |
| Step — Current | `var(--background-inverse-primary)` · `var(--double-color-outline-inverse)` | `.squircle--inverse` with the content re-pointed |
| Step — Upcoming | `var(--surface-on-white)` · `var(--text-and-icon-primary)` | `.squircle` (Standard) |
| Step — Cancelled | `var(--surface-on-white)` · `var(--text-and-icon-error)` | `.squircle` + `.progress-steps-list__step--cancelled` |
| Step — Skeleton | `var(--skeleton-on-white)` · shimmer `var(--skeleton-wave)` | `.squircle--skeleton`, content hidden |
| Disabled — every step | `var(--surface-on-white)` · `var(--text-and-icon-disabled)` | `.squircle--disabled` |
| Divider (`--progress-steps-list-divider`) | `var(--border-default)` | Unchanged in Disabled and Skeleton |
| DefaultItem title | `var(--text-and-icon-primary)` | The Cell's root colour |
| DefaultItem subtitle | `var(--text-and-icon-secondary)` | The Cell's `.cell-subtitle` |
| DefaultItem chevron | `var(--text-and-icon-primary)` | The Cell's end-slot colour |
| DefaultItem background | — | Transparent — re-pointed from the Cell's `Background/Primary`, so the cell sits on the host's surface like the squircles |
| DefaultItem — Disabled | `var(--text-and-icon-disabled)` | The Cell's `.is-disabled` — title, subtitle and chevron |
| Skeleton bars | `var(--skeleton-on-white)` · shimmer `var(--skeleton-wave)` | DS `.skeleton` |

Custom base: each status pair is re-pointable through
`--progress-steps-list-{completed · current · upcoming · cancelled}-fill` / `-content`; unset, the
values above (§ 3). The Disabled list ignores them — every step keeps the Squircle's own Disabled
values, a re-pointed Completed colour included.

- Contrast — the StepItem pairs are ProgressStepsCompact's (§ 6 there): Upcoming ≈ 15.7:1 Day,
  Current ≈ 12.9:1 Day; Cancelled `TextAndIcon/Error` ≈ 3.5:1 Day and Completed
  `TextAndIcon/Disabled` ≈ 1.9:1 Day sit under 4.5:1 — the status is spoken in words (§ 10).
- The Divider `Border/Default` on `Background/Primary` is low-contrast by design — it joins the
  steps, it does not carry information.

---

## 7. Typography

| Element | Style | Tokens | Scaling |
|---|---|---|---|
| Step number (Text type) | Body/Main Body — 16/20, Suisse Intl Book | `var(--text-body-main-body-*)` (set by `.squircle--s`) | `full` (Figma binds `SP/150% ratio`) |
| Step icon (Icon type) | — | 24 glyph, `var(--sp-s24)` | `full` |
| DefaultItem title | Body/Main Body — 16/20, Suisse Intl Book | `var(--text-body-main-body-*)` (`.cell-title`) | `full` |
| DefaultItem subtitle | Body/Compact Body — 14/16, Suisse Intl Book | `var(--text-body-compact-body-*)` (`.cell-subtitle`) | `full` |

Title and subtitle run one line with an ellipsis (the Cell's text rows); a multiline title takes
the Text Row's `.text-row--multiline`, and the row grows with it.

---

## 8. Spacing

Figma binds the paddings and gaps to `SP/100% ratio` (fixed), the squircle box and radius to
`SP/130% ratio` (cap130), the text to `SP/150% ratio`. The web follows the same families.

| Property | Token | @100% | Figma variable | Notes |
|---|---|---|---|---|
| Container width | — | fills the host | — | 360 in the Figma set; `width: 100%` |
| Gap between steps | — | 0 | `SP/100% ratio/s0` | The Divider joins the rows |
| Row padding — start / end | `var(--sp-100-s20)` | 20 | `SP/100% ratio/s20` | Custom base: `--progress-steps-list-padding-inline` |
| Row padding — top / bottom | `var(--sp-100-s0)` | 0 | `SP/100% ratio/s0` | |
| Row min height | `var(--sp-100-s64)` | 64 | — (unbound in Figma) | The squircle, the `s4` gap, a 16 line and the `s4` bottom |
| StartSlot → EndSlot gap | `var(--sp-100-s8)` | 8 | `SP/100% ratio/s8` | Custom base: `--progress-steps-list-gap` |
| StepItem box | `var(--sp-130-s40)` | 40 | `SP/130% ratio/s40` | The Squircle S |
| StepItem radius | `var(--sp-130-s16)` | 16 | `SP/130% ratio/s16` | The Squircle S |
| Squircle → Divider gap | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | StartSlot gap |
| StartSlot bottom padding | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | The Divider stops `s4` above the next squircle |
| Divider width | `var(--sp-s2)` | 2 | — (unbound in Figma) | Custom base: `--progress-steps-list-divider-width` |
| Divider radius | `var(--sp-100-s8)` | 8 | `SP/100% ratio/s8` | |
| EndSlot bottom padding | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | |
| EndSlot min height | `var(--sp-100-s24)` | 24 | — (unbound in Figma) | |
| DefaultItem height | `var(--sp-130-s40)` (min) | 40 | — (unbound in Figma) | The squircle's height, so the text block centres on it; the Cell's own `s10` / `s16` padding and `s48` floor are removed |
| DefaultItem gap (text → chevron) | `var(--sp-s8)` | 8 | `SP/100% ratio/s8` | The Cell's own slot gap |
| Skeleton bars | `var(--sp-100-s16)` / `var(--sp-100-s12)` high, `var(--sp-100-s120)` wide (the second) | 16 / 12 / 120 | — (unbound in Figma) | Radius `var(--sp-100-s8)`, gap and vertical padding `var(--sp-100-s4)` |

- The DefaultItem text rows keep the Cell's own `var(--sp-s2)` gap; the Figma instance draws `s4`
  — flagged for the Figma side (the Cell master is the contract).
- At 130 / 150 % the squircles stop at 52 (cap130) and the paddings stay put; the text grows, the
  rows grow with it.

---

## 9. Usage context

### When to use ProgressStepsList

- The full breakdown of an ordered flow, each step described — a courier delivery with addresses,
  people and orders; a multi-stop ride; a document check with what each step needs.
- The screen MoreButton opens from [ProgressStepsCompact](https://super-dollop-pzmo65r.pages.github.io/progress-steps-compact.md).
- A flow whose steps open their own details — DefaultItem with tapping on, or the team's own
  content with its own targets.

### When not to use

- Where only the position matters and space is tight — use ProgressStepsCompact.
- For a continuous value — a download, a timer. That is a Progress Bar.
- As a stepper the user taps through to move the flow — the steps report the flow; they do not
  advance it.
- For a list with no order, or for one step — use [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md) rows.

### Related components

- [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) — the StepItem.
- [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md) — DefaultItem.
- [ProgressStepsCompact](https://super-dollop-pzmo65r.pages.github.io/progress-steps-compact.md) — the compact row; the same StepItem.
- [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) — the loading bars.
- [IconContainer](https://super-dollop-pzmo65r.pages.github.io/icon-container.md) — the custom base's alternative StepItem.

---

## 10. Accessibility

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

- **A list of steps.** The steps are an ordered list; each step is one element read as its
  position and status, then its content: "Step 2 of 8, in progress. Pick up, 58 Zhandosov Street".
  The squircles, numbers, icons and Dividers are hidden from assistive tech.
- **The current step** says "in progress" in its prefix, and the list's phrase — a visually
  hidden `role="status"` paragraph — names it: "Step 2 of 8, Pick up, in progress". A change of the
  current step is announced once, politely. No `aria-current` — it would repeat the status.
- **Tappable DefaultItem** is a button whose label starts with the position and status: "Step 2 of
  8, in progress, Pick up, 58 Zhandosov Street, Button". Its hit area reaches 48 high (the cell is
  40).
- **The team's own content** keeps its components' own semantics — the step is a container, not
  one merged element, so a control inside stays a separate target; the step's position and status
  still come first.
- **Skeleton:** the phrase reads "Loading" (localized); the placeholders are hidden and nothing is
  focusable. No `aria-busy` — it would hold the live phrase back.
- **Disabled:** the steps keep their spoken position and status — the drawn squircles no longer
  show them. A tappable DefaultItem stays reachable and refuses activation: TalkBack "…, Button,
  Disabled", VoiceOver "…, Dimmed, Button". Web: `aria-disabled="true"`, and the click handler
  returns early.
- Reduced motion: the press and the shimmer zero at their tokens.
- Completed (≈ 1.9:1 Day) and Cancelled (≈ 3.5:1) step content sits under WCAG 1.4.3 for sighted
  users, and the status is shown by colour alone (WCAG 1.4.1) — the ProgressStepsCompact flag, the
  same StepItem. Owner ruling 2026-11-12: the Figma colours stay; the gap is accepted.

---

## 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` | `progress-steps-list` |
| Coverage unit | yes — one root, one DS component instance |
| Tap target model | none by default; multiple targets when DefaultItem is tappable — one per step |
| Actions | `open-step` (a tappable DefaultItem) |
| Internal targets | `step` — the DefaultItem button; its `data-ds-value` is the step's number |
| Emits value | yes — `data-ds-value` on the root is the current step's number ("2"; the last step's number when every step is done; absent when not started) |

Attribute model: `data-ds-state` = `standard`, `skeleton` or `disabled`; `data-ds-variant` is
absent (one variant). `data-ds-preset` is absent on the main preset and `custom` on an instance
built on the custom base (§ 3). RTL is the reading direction, not a variant. The nested Squircles
and DefaultItem are parts of this component, not coverage units of their own; the components a
team puts in EndSlot keep their own `data-ds-component` and count on their own. In Disabled no tap
event fires.

```html
<div class="progress-steps-list"
     data-ds-component="progress-steps-list" data-ds-component-id="delivery.route"
     data-ds-state="standard" data-ds-value="2">
  <p class="visually-hidden" role="status">Step 2 of 4, Pick up, in progress</p>
  <ol class="progress-steps-list__steps" role="list">
    <li class="progress-steps-list__item">
      <div class="progress-steps-list__start" aria-hidden="true">
        <span class="squircle squircle--s squircle--text progress-steps-list__step squircle--disabled"><span class="squircle__slot">1</span></span>
        <span class="progress-steps-list__divider"></span>
      </div>
      <div class="progress-steps-list__end">
        … the same tappable DefaultItem as step 2, data-ds-value="1" …
      </div>
    </li>
    <li class="progress-steps-list__item">
      <div class="progress-steps-list__start" aria-hidden="true">
        <span class="squircle squircle--s squircle--text progress-steps-list__step squircle--inverse"><span class="squircle__slot">2</span></span>
        <span class="progress-steps-list__divider"></span>
      </div>
      <div class="progress-steps-list__end">
        <button type="button" class="cell progress-steps-list__default"
                data-ds-action="open-step" data-ds-target="step" data-ds-value="2">
          <div class="cell-top-content">
            <div class="cell-start-text">
              <div class="text-row"><span class="text-row__text cell-title"><span class="visually-hidden">Step 2 of 4, in progress. </span>Pick up</span></div>
              <div class="text-row"><span class="text-row__text cell-subtitle">58 Zhandosov Street</span></div>
            </div>
            <div class="cell-end-slot"><svg class="cell-chevron" aria-hidden="true" …>cell-chevron</svg></div>
          </div>
        </button>
      </div>
    </li>
    … the last step has no __divider …
  </ol>
</div>
```

The list's phrase stays in the DOM; only its text changes. The step's position and status are a
visually hidden prefix inside the title, so a tappable DefaultItem's button name starts with them;
in the team's own content the prefix is the first child of `.progress-steps-list__end`. Tapping
is set per list: every DefaultItem is a `<button>` (tappable) or every one a `<div>` (the default,
no `data-ds-action`). A
cancelled step adds `progress-steps-list__step--cancelled` to the bare squircle. The Skeleton list:

```html
<div class="progress-steps-list progress-steps-list--skeleton"
     data-ds-component="progress-steps-list" data-ds-component-id="delivery.route"
     data-ds-state="skeleton">
  <p class="visually-hidden" role="status">Loading</p>
  <ol class="progress-steps-list__steps" role="list" aria-hidden="true">
    <li class="progress-steps-list__item">
      <div class="progress-steps-list__start">
        <span class="squircle squircle--s squircle--skeleton progress-steps-list__step"></span>
        <span class="progress-steps-list__divider"></span>
      </div>
      <div class="progress-steps-list__end progress-steps-list__placeholder">
        <div class="skeleton"></div>
        <div class="skeleton"></div>
      </div>
    </li>
    … four rows, the last without __divider …
  </ol>
</div>
```

When the steps land, the host renders the Standard list in the same root: it removes
`progress-steps-list--skeleton`, sets `data-ds-state="standard"` and writes the position into the
same phrase paragraph. The markup is the preview page's `list` macro
(`src/progress-steps-list.njk`).

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.2 | 2026-11-12 | Status Draft → Ready (owner, 2026-11-12) — unblocked by the owner's rulings of the same day. No contract change; Figma-side flags recorded above stay open on the Figma side. |
| 3.0.1 | 2026-11-12 | Completed and Cancelled content contrast and the colour-only status: the Figma colours stay — the gap is accepted by the owner (ruling), as on ProgressStepsCompact. |
| 3.0.0 | 2026-11-11 | Initial spec, from the Figma `[ProgressStepList] 3.0` page (`39122:7587`, file `uKPcsUAZ9fMW6H9KierbE9`): master `38825:9975`, set `ProgressStepsList` (`Skeleton = Off / On`), building blocks `ListStepItem` (`RTL` × `Skeleton`), `🔴StartSlot`, `🔵EndSlot`, `DefaultItem`; the StepItem shared with ProgressStepsCompact. A vertical list: rows `s20` at the sides, `s8` between the slots, at least 64 high; StartSlot = Squircle S + a vertical `Border/Default` Divider (`s2`, radius `s8`, `s4` below the squircle and above the next one, none on the last step); EndSlot = DefaultItem (a padding-free Cell, title + subtitle) or the team's own DS content. Owner rulings (2026-11-11): a Disabled state — the squircles Disabled, the Dividers unchanged, the EndSlot content responsible for its own Disabled; a tappable DefaultItem shows the `cell-chevron`; the custom base specified through web hooks (StepItem swap, status colours, Divider, paddings). Web build: `shared.css` + page (7 sections) + `capabilities.json` + registry row + manifest web entry (no node — no bridge carries it). Spec-reviewer and a11y-reviewer folded in: the custom Completed colour no longer reaches the Disabled list; team content with a control makes the step a container, not a merged element; `role="list"` on the `<ol>` (WebKit drops a `list-style: none` list), no `aria-current` (the prefix says "in progress"); the chevron `aria-hidden`; the list phrase appends no cancelled steps (each prefix says it). Flags: Completed / Cancelled content under WCAG 1.4.3 and status by colour alone (1.4.1) — Figma colours, for the owner; the DefaultItem text-row gap is `s4` in the Figma instance, `s2` in the Cell master. |

---

# ProgressStepsList — Accessibility

ProgressStepsList is the full list of an ordered flow's steps. Assistive tech gets it as an
**ordered list**: one element per step, read as the step's **position and status** first, then its
content. A spoken **list phrase** names the current step and announces when it changes. The
squircles, numbers, icons and Dividers are drawn only — they are not announced on their own.

## The step's position and status

Each step starts with a localized prefix: "Step N of M, status".

| Status | Prefix |
|---|---|
| Completed | "Step 1 of 8, completed" |
| Current | "Step 2 of 8, in progress" |
| Upcoming | "Step 3 of 8, upcoming" |
| Cancelled | "Step 4 of 8, cancelled" |

- **M counts the whole flow**, the same as the list draws — every step is drawn.
- The prefix is followed by the step's content: DefaultItem's title and subtitle, or the team's own
  content in its own reading order. Example: "Step 2 of 8, in progress. Pick up, 58 Zhandosov
  Street, Residential Complex".
- **Status words** — completed, in progress, upcoming, cancelled — are localized strings. The
  status is drawn by colour alone; the prefix is where it is said. The current step is not marked
  a second way (no web `aria-current`): its prefix already says "in progress", and the list phrase
  names it too.
- The step's number or icon in the squircle is not read: the prefix already gives the number, and
  an icon (person-courier, courier-walk…) says nothing on its own.

Label rule: ✅ "Step 2 of 8, in progress. Pick up, 58 Zhandosov Street", ❌ "2, Pick up" (the drawn
number read as text, no status), ❌ "Pick up" with no position.

## The list phrase

The list carries one phrase that names the current step — the ProgressStepsCompact position
rule: "Step 2 of 8, Pick up, in progress"; with no step in progress, "3 of 8 steps completed",
"8 steps, not started", "All 8 steps completed" (none cancelled), "All steps completed" (every
step done, some cancelled) or "All 8 steps cancelled". Unlike ProgressStepsCompact, the cancelled
steps are not appended: every step is drawn, and each cancelled step says "cancelled" in its own
prefix. The phrase is not drawn. It is the list's live region: a change of the current step while
the screen is open is announced once, politely, and the first render is not announced. In
browse / swipe mode it is read once before the list — a summary, then the steps; the current step
is heard again in its own prefix, a deliberate repeat.

## Tappable DefaultItem

When the host turns tapping on, DefaultItem is a button and the step's one element.

- **Label:** the prefix, then the title and subtitle — "Step 2 of 8, in progress, Pick up, 58
  Zhandosov Street". The chevron is not announced.
- **Action:** opens the step's details (the host's navigation).
- **Touch target:** the cell is 40 high (the squircle's height). Its hit area reaches 48 —
  4 above and below, into the row's own spacing — without changing the drawn layout. The full row
  width is the cell's.

## The team's own content

Content a team puts in EndSlot keeps its components' own semantics — a Button stays a button, a
Tag stays text. The step is then a **container**, not one merged element: the prefix is its first
element, then the content's own elements in reading order, so the position and status are read
before the content and every control inside stays a separate target. A step whose team content
holds no control may still be merged into one element, like DefaultItem. Hit targets inside follow
each component's own contract.

## Focus order

The steps top to bottom, in LTR and RTL; inside a step, its content in reading order. Only
tappable elements take keyboard focus — the list itself is not a stop. In swipe navigation the
list phrase is the first stop on Android (a summary node before the list); on iOS and the web
it is not a separate stop (iOS posts it as an announcement; on the web it is a text line read in
browse mode).

---

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| List | `semantics { collectionInfo = CollectionInfo(rowCount = 8, columnCount = 1) }` | — | List — TalkBack reads "List, 8 items" on entry | — |
| List phrase | A summary node before the list with `contentDescription = "Step 2 of 8, Pick up, in progress"; liveRegion = LiveRegionMode.Polite` — a swipe stop by design, not drawn but laid out (a live region needs a size to be announced) | — | — | — |
| Step (not tappable) | `semantics(mergeDescendants = true) { collectionItemInfo = CollectionItemInfo(rowIndex, 1, 0, 1) }`, the prefix as its first text — "Step 2 of 8, in progress. Pick up, 58 Zhandosov Street" | — | — | — |
| Step (team content with a control) | Not merged: `collectionItemInfo` on the step, the prefix its first text node, then the content's own nodes (a Button keeps `Role.Button` and its click) | — | — | — |
| Step (tappable DefaultItem) | The merged node with `role = Role.Button; onClick(…)` — "Step 2 of 8, in progress, Pick up, 58 Zhandosov Street" | — | `Role.Button` | TalkBack's own "Double-tap to activate" |
| StepItem / Divider | Not nodes — `clearAndSetSemantics {}` on the StartSlot | — | — | — |
| Chevron | Not a node | — | — | — |

- **Current step:** its prefix says "in progress"; Compose has no "current" semantic of its own.
- **Hit area:** a tappable DefaultItem is 40 high; the touch region is extended to 48 around it
  (a touch-bounds extension on the clickable), not a larger laid-out box — `minimumInteractiveComponentSize()`
  would grow the row and move the Divider. Verified on a device.
- **Live region:** the list phrase node stays in the tree across updates — only its description
  changes. Whether TalkBack speaks it on first appearance is verified on a device; if it does, the
  live region is set after the first render.

### Edge states

- **Disabled:** every step keeps its prefix — the drawn squircles no longer show the status, the
  prefix does. A tappable DefaultItem stays a node, reachable by swipe, with `disabled()`: TalkBack
  reads "Step 2 of 8, in progress, Pick up, …, Button, Disabled" and ignores the double-tap. The
  team's own content announces its own Disabled state.
- **Skeleton / loading:** the list phrase reads "Loading" (localized); the placeholder rows are
  not nodes and nothing is focusable. When the steps land, the phrase changes to the position —
  read once.
- **Error / Cancelled:** a cancelled step says "cancelled" in its prefix; its red content is not
  the only signal.
- **Text scaling:** the title and subtitle scale to 150 %; the rows grow. The squircle stops at
  130 % — a one- or two-digit number still fits.
- **RTL:** the reading and focus order are unchanged; the chevron and RTL-marked step icons mirror.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| List | A container — `accessibilityElement(children: .contain)` | — | — | — |
| List phrase | Not an element on screen; posted as an announcement on a change (below) | — | — | — |
| Step (not tappable) | `accessibilityElement(children: .combine)` with the prefix first — "Step 2 of 8, in progress. Pick up, 58 Zhandosov Street" | — | `.staticText` | — |
| Step (team content with a control) | `accessibilityElement(children: .contain)`: the prefix as the first element, then the content's own elements (a Button keeps `.button` and its activation) | — | — | — |
| Step (tappable DefaultItem) | The combined element — "Step 2 of 8, in progress, Pick up, 58 Zhandosov Street" | — | `.button` | "Opens the step's details." |
| StepItem / Divider | `accessibilityHidden(true)` | — | — | — |
| Chevron | Hidden | — | — | — |

- **Current step:** its prefix says "in progress"; no trait stands for "current" on iOS, and
  `.isSelected` would read "Selected", which the step is not.
- **Hit area:** the cell's touch region grows to 48 around the 40 cell (UIKit
  `point(inside:with:)`; SwiftUI a non-layout overlay) — at least the 44 HIG minimum. Verified on
  a device.
- **A change of the current step** while the screen is open: the host posts the list phrase as a
  queued announcement — an attributed string with `.accessibilitySpeechQueueAnnouncement = true`
  (a plain `.announcement` interrupts current speech).

### Edge states

- **Dimmed / Disabled:** every step keeps its prefix. A tappable DefaultItem stays an element with
  `.notEnabled` — "Step 2 of 8, in progress, Pick up, …, Dimmed, Button" — and ignores the
  double-tap; no hint.
- **Skeleton / loading:** one element labelled "Loading"; the placeholders are hidden. When the
  steps land, the host posts the list phrase as a queued announcement.
- **Error / Cancelled, Dynamic Type, RTL:** as on Android.

---

## Web preview (reference)

- **The list** is an `<ol class="progress-steps-list__steps" role="list">` of `<li>` — the browser
  announces "list, 8 items". `role="list"` is explicit because WebKit drops the list semantics of a
  `list-style: none` list. No `aria-current` — the prefix says "in progress" (above).
- **The prefix** is a `.visually-hidden` span ("Step 2 of 8, in progress. ") — inside DefaultItem's
  title, so a tappable cell's button name starts with it; in the team's own content, the first
  child of `.progress-steps-list__end`. The StartSlot (squircle and Divider) is `aria-hidden="true"`.
- **The list phrase** is a `.visually-hidden` paragraph with `role="status"` before the `<ol>`. It
  stays in the DOM across updates — only its text changes — so a change of the current step is
  announced once, politely, and the first render is not. No `aria-label` on the root `<div>`.
- **Tappable DefaultItem** is the Cell as a native `<button type="button">`: Enter and Space
  activate it; its focus ring is the Cell's inset `s2` outline in `var(--text-and-icon-primary)`.
  The chevron is `aria-hidden="true"`.
  The 48 hit area is a transparent `::before` layer, `s4` beyond the cell's top and bottom edges.
  A not-tappable DefaultItem is a `<div>` — no focus, no press.
- **Disabled:** the root carries `.progress-steps-list--disabled`; the prefixes and the phrase are
  unchanged. A tappable DefaultItem takes `aria-disabled="true"` (not `disabled` — that would drop
  it from the tab order) and the Cell's `.is-disabled`; the click handler returns early while it is
  set — `aria-disabled` does not stop a native button's `Enter` / `Space` click.
- **Skeleton:** the phrase reads "Loading"; the placeholder `<ol>` is `aria-hidden="true"` and takes
  no focus. No `aria-busy` — the paragraph is the live region, and a busy ancestor would hold its
  announcement back (the NumericStepper rule).
- **Forced colours** (Windows High Contrast): squircle fills are forced to Canvas. The Current step
  draws an `s2` outline in `CanvasText`, so the position stays visible; the Divider takes
  `GrayText`; skeleton placeholders keep a 1px `GrayText` outline.
- **Reduced motion:** the Cell press (`--component-push-highlight`) and the shimmer
  (`--pattern-shimmer`) zero at their tokens; nothing else moves.

### Colour and contrast

Measured from the primitives (`tokens/generated/primitives.css`); the StepItem pairs are
ProgressStepsCompact's.

| Pairing | Tokens | Measured |
|---|---|---|
| Upcoming step | `--text-and-icon-primary` on `--surface-on-white` | ≈ 15.7:1 Day / ≈ 13.2:1 Night |
| Current step | `--double-color-outline-inverse` on `--background-inverse-primary` | ≈ 12.9:1 Day / ≈ 17.8:1 Night |
| Cancelled step | `--text-and-icon-error` on `--surface-on-white` | ≈ 3.5:1 Day / ≈ 3.8:1 Night — under 4.5:1 for 16px text |
| Completed step | `--text-and-icon-disabled` on `--surface-on-white` | ≈ 1.9:1 Day / ≈ 7.6:1 Night — the Squircle Disabled dim |
| DefaultItem title / chevron | `--text-and-icon-primary` on the host (`--background-primary`) | The Cell's own pairing |
| DefaultItem subtitle | `--text-and-icon-secondary` on the host | The Cell's own pairing |
| Divider | `--border-default` on `--background-primary` | Low by design — a joining line, no information |

**Owner ruling (2026-11-12) — WCAG 1.4.3 and 1.4.1.** The Completed (1.9:1 Day) and Cancelled (3.5:1)
numbers carry meaning; the prefix covers screen-reader users, not sighted low-vision users. The
status is also shown by colour alone (1.4.1): Completed, Upcoming and Cancelled differ only in
fill and number colour, and in forced colours only the Current step keeps a mark (its outline) —
the other three look alike, and in the Disabled list none is marked. The colours are the Figma
contract, kept as drawn — the owner accepts the gap, as on ProgressStepsCompact. A non-colour mark
(an icon, a forced-colours outline style per status) was considered and not taken.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.0 | 2026-11-11 | Initial accessibility spec: an ordered list, one element per step read as "Step N of M, status" then its content; the list phrase (a `role="status"` paragraph) names the current step and announces its change; tappable DefaultItem a button with the prefix in its name and a 48 hit area around the 40 cell; the team's content keeps its own semantics (the step a container, not a merged element); `role="list"` on the `<ol>`, no `aria-current`; Disabled keeps every prefix, a tappable cell reachable with `aria-disabled`; Skeleton reads "Loading" with no `aria-busy`. |

---

## Machine contract — `specs/components/progress-steps-list/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": "progress-steps-list",
  "name": "ProgressStepsList",
  "version": "3.0.2",
  "description": "The full vertical list of an ordered flow's steps (a courier route): each step a row — a Squircle S whose state follows the step status, over a vertical Divider that runs down to the next step (none on the last), and beside it the step's description in EndSlot: DefaultItem (a padding-free Cell with a title and subtitle; not tappable by default, tappable with the cell-chevron) or a team's own DS layout. An ordered list for assistive tech, each step read as \"Step N of M, status\" first; a role=\"status\" phrase names the current step.",
  "files": {
    "spec": "specs/components/progress-steps-list/progress-steps-list.md",
    "a11y": "specs/components/progress-steps-list/progress-steps-list-a11y.md",
    "preview": "src/progress-steps-list.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "[ProgressStepList] 3.0 — new component file (page 39122:7587). Master 38825:9975; set ProgressStepsList 38898:243673 — Skeleton (Off 38825:9686 · On 38898:243704). Building blocks: ListStepItem 38832:10703 (RTL Off · On × Skeleton Off · On), 🔴StartSlot 38825:8274, 🔵EndSlot 38825:8330, DefaultItem 38825:10067 ([Cell] 3.4 / [CustomCell] 3.3), StepItem 38767:1381 (shared with ProgressStepsCompact). StepItemCouriers 38825:9158 is a product example of a team's own EndSlot content, not part of the component. The component key is not captured yet — the desktop MCP read gives node-ids only.",
    "fileKey": "uKPcsUAZ9fMW6H9KierbE9",
    "capturedAt": "2026-11-11"
  },
  "root": {
    "class": "progress-steps-list",
    "dataDsComponent": "progress-steps-list"
  },
  "anatomy": {
    "phrase": {
      "class": "visually-hidden",
      "notes": "A <p class=\"visually-hidden\" role=\"status\"> — the root's first child: names the current step (\"Step 2 of 8, Pick up, in progress\"; \"Loading\" in Skeleton). It stays in the DOM — only its text changes, so a change is announced once and the first render is not. No aria-label on the root."
    },
    "steps": {
      "class": "progress-steps-list__steps",
      "notes": "An <ol role=\"list\"> after the phrase (the explicit role — WebKit drops a list-style: none list's semantics); one li per step in flow order. aria-hidden only in Skeleton."
    },
    "item": {
      "class": "progress-steps-list__item",
      "count": "2 and up",
      "notes": "ListStepItem, an <li>: StartSlot + EndSlot, gap --progress-steps-list-gap (var(--sp-100-s8)), padding var(--sp-100-s0) --progress-steps-list-padding-inline (var(--sp-100-s20)), min-height var(--sp-100-s64)."
    },
    "start": {
      "class": "progress-steps-list__start",
      "notes": "🔴StartSlot, aria-hidden: a column, items centred, gap var(--sp-100-s4), padding-bottom var(--sp-100-s4), stretched to the row's height."
    },
    "step": {
      "class": "progress-steps-list__step",
      "notes": "A Squircle S wearing its own classes plus .progress-steps-list__step: squircle squircle--s progress-steps-list__step (+ squircle--text for a number; an icon step is the bare type, the inline SVG at s24, fill currentColor; a directional glyph — courier-walk — carries .progress-steps-list__icon--rtl). The status classes are the stepStatus axis. The ProgressStepsCompact StepItem (38767:1381)."
    },
    "divider": {
      "class": "progress-steps-list__divider",
      "count": "steps − 1",
      "notes": "A vertical line under the squircle in StartSlot: width --progress-steps-list-divider-width (var(--sp-s2)), radius var(--sp-100-s8), background --progress-steps-list-divider (--border-default), flex: 1 1 0 — it fills the row and ends var(--sp-100-s4) above the next squircle. Absent on the last step (and hidden by :last-child). Static; unchanged in Disabled and Skeleton."
    },
    "end": {
      "class": "progress-steps-list__end",
      "notes": "🔵EndSlot: fill width, min-height var(--sp-100-s24), padding-bottom var(--sp-100-s4). Holds DefaultItem or the team's own DS content; with team content its first child is the .visually-hidden step prefix."
    },
    "defaultItem": {
      "class": "progress-steps-list__default",
      "notes": "DefaultItem — a Cell (.cell.progress-steps-list__default) with padding, border and background removed and .cell-top-content at min-height var(--sp-130-s40), so the text centres on the squircle. A div when not tappable (the default — no pointer, no press); a <button type=\"button\"> when tappable, with the cell-chevron in .cell-end-slot, data-ds-action=open-step, data-ds-target=step, data-ds-value = the step number, and a ::before reaching var(--sp-100-s4) beyond its top and bottom (48 hit area). The title starts with a .visually-hidden prefix \"Step N of M, status. \"."
    },
    "placeholder": {
      "class": "progress-steps-list__placeholder",
      "onlyWhen": {
        "skeleton": [
          true
        ]
      },
      "notes": "Skeleton only: EndSlot with two DS .skeleton bars — full width × var(--sp-100-s16), then var(--sp-100-s120) × var(--sp-100-s12); radius var(--sp-100-s8); gap and padding-block var(--sp-100-s4)."
    }
  },
  "axes": {
    "skeleton": {
      "title": "Skeleton",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".progress-steps-list--skeleton",
        "mechanism": "the root modifier + data-ds-state=\"skeleton\"; four rows of .squircle--skeleton steps and .progress-steps-list__placeholder bars, a Divider under the first three, the <ol> aria-hidden; the phrase reads \"Loading\" — no aria-busy"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Skeleton",
        "values": {
          "false": "Off",
          "true": "On"
        }
      },
      "constraints": [
        "Four placeholder rows whatever the flow's length — static in the main preset; a matching shape is a custom-base job."
      ]
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "dir=\"rtl\" on the root or an ancestor: each row reverses (StartSlot on the right), the text aligns to the start side; the Cell's chevron and any .progress-steps-list__icon--rtl step icon mirror under :dir(rtl)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "RTL",
        "values": {
          "false": "Off",
          "true": "On"
        }
      }
    },
    "stepStatus": {
      "title": "Step status",
      "type": "enum",
      "values": [
        "completed",
        "current",
        "upcoming",
        "cancelled"
      ],
      "default": "upcoming",
      "css": {
        "mechanism": "per step, on the step's squircle: completed .squircle--disabled · current .squircle--inverse (content --double-color-outline-inverse) · upcoming the bare .squircle · cancelled the bare .squircle + .progress-steps-list__step--cancelled (--squircle-content = --text-and-icon-error)"
      },
      "figma": {
        "kind": "none",
        "notes": "The Status variant of the nested StepItem 38767:1381, set per step instance."
      },
      "constraints": [
        "One current step per list at most; its prefix says \"in progress\" — no aria-current.",
        "The status is drawn by colour alone; the step prefix speaks it."
      ]
    },
    "stepContent": {
      "title": "Step content",
      "type": "enum",
      "values": [
        "text",
        "icon"
      ],
      "default": "text",
      "css": {
        "mechanism": "text: .squircle--text with the step number in .squircle__slot · icon: the bare .squircle--s with the step's SVG (var(--sp-s24)) in .squircle__slot"
      },
      "figma": {
        "kind": "none",
        "notes": "The nested Squircle's own Type (Text | Icon)."
      },
      "constraints": [
        "One content type per list."
      ]
    },
    "endSlot": {
      "title": "EndSlot content",
      "type": "enum",
      "values": [
        "default-item",
        "custom"
      ],
      "default": "default-item",
      "css": {
        "mechanism": "default-item: .cell.progress-steps-list__default inside .progress-steps-list__end · custom: the team's own DS markup inside .progress-steps-list__end, preceded by the .visually-hidden step prefix"
      },
      "figma": {
        "kind": "none",
        "notes": "The 🔵EndSlot slot (38825:8330): DefaultItem 38825:10067 by default; StepItemCouriers 38825:9158 is a product example of custom content."
      },
      "constraints": [
        "Custom content keeps its components' own semantics, targets, states (Disabled included) and bottom spacing; its components keep their own data-ds-component."
      ]
    },
    "tappable": {
      "title": "Tappable (DefaultItem)",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "false: DefaultItem is a div.cell, no end slot · true: a button.cell with the cell-chevron in .cell-end-slot, the Cell's press (--component-push-highlight → --shadow-pressed) and inset focus ring, data-ds-action=open-step"
      },
      "figma": {
        "kind": "none",
        "notes": "DefaultItem description: click behaviour off by default; when enabled the entire CustomCell surface is clickable. The chevron is an owner ruling (2026-11-11)."
      },
      "onlyWhen": {
        "endSlot": [
          "default-item"
        ]
      }
    },
    "subtitle": {
      "title": "Subtitle (DefaultItem)",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "the second .text-row (.cell-subtitle) present or absent"
      },
      "figma": {
        "kind": "none",
        "notes": "The Cell's own subtitle row."
      },
      "onlyWhen": {
        "endSlot": [
          "default-item"
        ]
      }
    },
    "paddingInline": {
      "title": "Row padding — start / end (custom base)",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-100-s20",
      "css": {
        "customProperty": "--progress-steps-list-padding-inline"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — custom padding."
      },
      "constraints": [
        "Custom base only; any SP token."
      ]
    },
    "gap": {
      "title": "StartSlot → EndSlot gap (custom base)",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-100-s8",
      "css": {
        "customProperty": "--progress-steps-list-gap"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — custom padding."
      },
      "constraints": [
        "Custom base only; any SP token."
      ]
    },
    "dividerColour": {
      "title": "Divider colour (custom base)",
      "type": "text",
      "default": "--border-default",
      "customizable": "any semantic border token",
      "css": {
        "customProperty": "--progress-steps-list-divider"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — the Divider styled."
      },
      "constraints": [
        "Custom base only."
      ]
    },
    "dividerWidth": {
      "title": "Divider width (custom base)",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s2",
      "css": {
        "customProperty": "--progress-steps-list-divider-width"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — the Divider styled."
      },
      "constraints": [
        "Custom base only; any SP token."
      ]
    },
    "statusColours": {
      "title": "Status colours (custom base)",
      "type": "text",
      "default": "unset — the Squircle states' own values",
      "customizable": "any semantic token per status, contrast checked",
      "css": {
        "mechanism": "--progress-steps-list-{completed|current|upcoming|cancelled}-fill and -content on the instance; consumed on the step squircles with the Squircle state's value as fallback"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — state colours overridden."
      },
      "constraints": [
        "Custom base only. Squircle steps; a swapped StepItem colours itself."
      ]
    },
    "stepItem": {
      "title": "StepItem (custom base)",
      "type": "enum",
      "values": [
        "squircle",
        "icon-container",
        "other-ds-component"
      ],
      "default": "squircle",
      "css": {
        "mechanism": "squircle: the main preset · icon-container: .progress-steps-list__step.progress-steps-list__step--icon-container with the SVG (s24) and its own color · other: any DS component carrying .progress-steps-list__step"
      },
      "figma": {
        "kind": "none",
        "notes": "Custom base — StepItem swappable (IconContainer named in the Figma description)."
      },
      "constraints": [
        "A swapped StepItem shows the status in more than colour where it matters; the spoken prefix is unchanged."
      ]
    }
  },
  "states": {
    "default": [
      "standard",
      "skeleton",
      "disabled"
    ],
    "defaultItem": [
      "standard",
      "pressed",
      "focus-visible",
      "disabled"
    ]
  },
  "analytics": {
    "dataDsComponent": "progress-steps-list",
    "actions": [
      "open-step"
    ],
    "targets": [
      "step"
    ],
    "valueAttr": "data-ds-value",
    "notes": "Coverage unit. data-ds-state = standard, skeleton or disabled; no data-ds-variant; data-ds-preset absent on the main preset, \"custom\" on the custom base. data-ds-value on the root = the current step's number (N when every step is done; omitted when not started); on a tappable DefaultItem = that step's number. The nested Squircles and DefaultItem are parts; team content in EndSlot keeps its own data-ds-component. In Disabled no tap fires."
  },
  "rtl": {
    "supported": true,
    "notes": "StartSlot on the right, EndSlot to its left, the text start-aligned (right); the chevron and RTL-marked step icons (courier-walk) mirror; numbers and other icons do not; the reading and focus order are unchanged."
  },
  "constraints": [
    "Markup order: root > p.visually-hidden[role=status] (the phrase) > ol.progress-steps-list__steps > li.progress-steps-list__item > div.progress-steps-list__start[aria-hidden] (step + divider) + div.progress-steps-list__end (DefaultItem or team content). The last step has no __divider.",
    "Every step's position and status is a .visually-hidden prefix (\"Step N of M, status. \") — inside DefaultItem's .cell-title, or the first child of __end with team content; M counts the whole flow.",
    "Paddings and gaps on the --sp-100-* family (Figma SP/100% ratio); the squircle box and radius cap at 130 % (--sp-130-*); the text scales in full.",
    "Disabled (owner, 2026-11-11; not in Figma): .progress-steps-list--disabled + data-ds-state=\"disabled\"; every step .squircle--disabled in place of its status class; Dividers unchanged; DefaultItem the Cell's .is-disabled, a tappable one aria-disabled=\"true\" (reachable, no press, no tap — the handler guards); team content handles its own Disabled.",
    "No ancestor of a tappable DefaultItem clips overflow vertically — the ::before 48 hit area reaches s4 beyond the cell.",
    "The DefaultItem text rows keep the Cell's var(--sp-s2) gap; the Figma instance draws s4 — flagged for the Figma side.",
    "Completed (≈ 1.9:1 Day) and Cancelled (≈ 3.5:1) content is under WCAG 1.4.3, and the status is colour alone (WCAG 1.4.1) — the Figma colours, flagged for the owner.",
    "Team content with a control: the step is a container (TalkBack not merged, VoiceOver children: .contain), the prefix first — never one merged element that swallows the control.",
    "Neither mobile platform ships it — no manifest node binding until a platform bridge carries [ProgressStepList] 3.0."
  ]
}
```
