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

---

# ProgressStepsCompact · Oymyakon DS 3

> A compact row of steps that shows how far a multi-step flow has progressed — up to four steps
> joined by connectors (a Divider, or a ProgressBar), with a trailing arrow to the full list when the flow has more.

**Version:** 3.0.2 · **Status:** Ready · **Figma:** `[ProgressStepCompact] 3.0` (file `uKPcsUAZ9fMW6H9KierbE9`, page `38755:775`; master `38770:2915`, variant set `ProgressStepsCompact` `38767:1508`, custom base `CustomProgressStepsCompact` `38824:5842`; building blocks `StepItem` `38767:1381`, `MoreButton` `38786:3606`)

---

## 1. Description

ProgressStepsCompact shows where the user is in a flow made of ordered steps — a courier delivery,
for example: go to the sender → pick up → go to the recipient → drop off. Each step is a small
squircle — holding the step's number or an icon — whose look follows the step's status (completed, current, upcoming, cancelled), and a
thin connector joins neighbouring steps — a Divider by default, or a ProgressBar that fills to show
how far the flow has moved between them — so the row reads as a path at a glance.

The component is the compact form for tight placements — a screen header, a card, a bottom
sheet's top. It shows at most **four** steps; a fifth position is only ever the **MoreButton**,
not a fifth step. The row does not scroll and does not wrap — when the flow has more steps, they
go behind MoreButton: an arrow that opens the full list of steps in a separate view, with a "+N" counter
of the steps hidden behind it. The full list itself is the sibling component
`[ProgressStepList] 3.0` — a separate component, not part of this one.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | — | — | Not in the platform bridge (manifest seed, 2026-11-10) |
| Android | — | — | Not in the platform bridge (manifest seed, 2026-11-10). The Figma connector is a Divider (`Border/Default`); the ProgressBar option follows `[ProgressBar] 3.0` S-size (Figma page `35163:9013`) |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.progress-steps-compact`) | 3.0.0 | Reference implementation, this repo; preview page `src/progress-steps-compact.njk` — the first build of this component on any platform |

---

## 2. Anatomy

```
ProgressStepsCompact (.progress-steps-compact)   fill width (min / max optional) · row · s20 left/right · s12 top/bottom · gap s4
├── Step 1 (.squircle.squircle--s)               StepItem — Squircle S, state from the step status
├── Connector                                    s2 high · fills the space between · one kind per row:
│     Divider (.progress-steps-compact__divider)       the default — Border/Default
│     ProgressBar (.progress-steps-compact__progress)  ProgressBar S, filled to the progress between steps
├── Step 2 … Step 4                              StepItem
├── Connector (More)                             Steps = More only
└── MoreButton (.progress-steps-compact__more)   Steps = More only — Squircle S + chevron + Indicator "+N"
    └── Counter (.indicator.indicator--m)        optional — "+N" hidden steps, s4 outside the top-end corner
```

| Part | Required | Description |
|---|---|---|
| **Container** | Required | A horizontal row, items centred vertically: padding `s20` left/right and `s12` top/bottom, gap `s4` between every item. Full width by default; a min / max width and an alignment are optional (§ 3) |
| **StepItem** | Required, 2–4 | One step: a [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) S (`s40` box, radius `s16`). Its State follows the step status (§ 4). The content is the step's number (Text type — the Figma default) or an icon (Icon type, the step's pictogram) |
| **Connector** | Required between steps | A line between two steps (`s2` high, radius `s16`), centred on the steps' midline; it takes the free width (§ 8), and with two steps it has a maximum length of `s48`, so the pair keeps its own width instead of spanning the screen. Two kinds, one per row (§ 3): **Divider** — the default, a static `Border/Default` line (the Figma `Divider`); **ProgressBar** — a [ProgressBar] 3.0 S whose fill runs from the start edge to `--progress-steps-compact-value` (0–100), empty when unset, style Primary by default (§ 6) |
| **MoreButton** | Steps = More only (optional on the custom base) | The fifth position — the only thing that ever takes it: a Squircle S Standard with the `chevron-right` icon. It opens the full list of steps in a separate view (the host's navigation) |
| **Counter** | On with Steps = More (off only in the custom base) | An [Indicator](https://super-dollop-pzmo65r.pages.github.io/indicator.md) M of the Number type on MoreButton — "+N", the number of steps hidden behind the arrow — anchored to the top-end corner and offset `s4` outside it |

- The ProgressBar connector is drawn inside this component, matching Figma `[ProgressBar] 3.0` S-size
  (page `35163:9013`): the DS ProgressBar is still planned (owner, 2026-11-10). When it ships, the
  bar becomes its S instance with no visual change.
- **Progress between steps** (ProgressBar connector). Each bar shows how far the flow has moved from the step before it
  to the step after it — a host that tracks it (distance, time) sets the value; a host that does
  not leaves every bar empty. The bar's style follows the row: Primary by default, the Disabled
  and Skeleton styles in those states, any other style on the custom base (§ 3).
- The steps are not separate controls. The only interactive part is MoreButton.

### Which steps show — the first four

A flow of up to four steps shows them all (`Steps = 2 · 3 · 4`). A longer flow uses
`Steps = More`: **steps 1–4 and MoreButton** — the same four throughout the flow (owner, 2026-11-10).

1. The four drawn steps are the flow's first four. They do not shift or slide as the flow moves on
   — the row has no scrolling and no window that follows the current step.
2. The Counter is "+N", N = the number of steps after the fourth (the flow's length − 4).
3. While the current step is one of the first four, it is drawn as Current. Once the flow moves past
   the fourth step, the four drawn steps are all passed (Completed or Cancelled) and the current
   step is behind the arrow; the row's spoken phrase still names it (§ 10), and MoreButton opens
   the full list where it is shown.

The four-step limit and the fixed first four are a deliberate constraint of the standard component.
A team changes them — another limit, a window that follows the current step — only in the custom
base `CustomProgressStepsCompact` (§ 3). The Figma component description still carries an older
window rule ("1 passed step, the current one and the next ones") and "Steps = 3–5"; this section
supersedes both — flagged for the Figma side.

### RTL layout

The Figma set stages `RTL = On` for every Steps value: the row runs right to left — step 1 at the
right edge, MoreButton at the left. MoreButton's chevron flips (`chevron-right` reads as
`chevron-left`); its Counter stays on the top-end corner, which is the top-left in RTL. The step
content does not mirror: numbers and icons read the same either way. With two steps, the row hugs
the right edge.

On the web: `dir="rtl"` on the root or an ancestor reverses the row; the chevron
(`.progress-steps-compact__more-icon`) mirrors horizontally (`scaleX(-1)`) under `:dir(rtl)`, and
so does an RTL-marked step icon (`.progress-steps-compact__icon--rtl`, § 3). The Counter is set
`direction: ltr` with `unicode-bidi: isolate`, so it reads "+2" in RTL, not "2+".

---

## 3. Variants and sizes

One size. Three axes in the Figma set (Steps, RTL, Skeleton); Counter is a MoreButton property and Step
content the nested Squircle's Type. The custom base follows the table.

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Steps** | `2` · `3` · `4` · `more` | `2` (Figma) | The number of StepItem children; `more` = four steps + `.progress-steps-compact__more`. `.progress-steps-compact--steps-2` caps the connector at `s48` |
| **Connector** | `divider` · `progress-bar` | `divider` | The element between steps: `.progress-steps-compact__divider` or `.progress-steps-compact__progress` (+ its `__progress-fill`). One kind per row — not a Figma property (owner, 2026-11-10) |
| **Alignment** | `start` · `center` · `end` | `start` | The bare root, `.progress-steps-compact--align-center`, `.progress-steps-compact--align-end` — not a Figma property (§ 3, Width and alignment) |
| **Width** | full · min · max | full | `width: 100%`; `--progress-steps-compact-min-width` / `--progress-steps-compact-max-width` on the instance or an ancestor — not a Figma property |
| **RTL** | `off` · `on` | `off` | `dir="rtl"` on the root or an ancestor |
| **Skeleton** | `off` · `on` | `off` | `.progress-steps-compact--skeleton` + four `.squircle--skeleton` steps, no MoreButton (§ 4) — Figma stages it with `Steps = 4` only |
| **Counter** (MoreButton) | `on` · `off` | `on` | The Indicator element present or absent inside MoreButton. With Steps = More there is at least one step behind the arrow, so the standard component keeps it on; off is a custom-base choice |
| **Step content** | `text` · `icon` | `text` | The Squircle's own Type: `.squircle--text` (a number) or the bare `.squircle--s` with an icon in the slot (Icon is the Squircle's default type, no modifier) |

| Steps | What shows | Connector width at a 360 host |
|---|---|---|
| **2** | Two steps, one connector of at most `s48` — the pair is 136 wide at 100 % and sits at the start edge (or where the alignment puts it) | `s48` (the cap) |
| **3** | Three steps, two connectors, spread across the width | 92 |
| **4** | Four steps, three connectors | ≈ 45.3 |
| **More** | Four steps, four connectors, MoreButton | 22 |

### Connector — Divider or ProgressBar

The steps are joined by one kind of connector per row (owner, 2026-11-10):

| Connector | When | Look |
|---|---|---|
| **Divider** (default) | The row shows where the user is; nothing moves between steps | A static line, `Border/Default`. It keeps its colour in every state — Disabled and Skeleton leave it as is |
| **ProgressBar** | The flow tracks the progress between two steps (distance, time) | A ProgressBar S: track + fill to `--progress-steps-compact-value`; empty when unset. Style Primary; the Disabled and Skeleton styles in those states; any style on the custom base |

Both share one box — `s2` high, radius `s16`, the free width, the two-step cap — so switching the
kind does not move the layout.

### Two steps — the connector's maximum length

With `Steps = 2` the connector does not stretch: its length is capped at
`--progress-steps-compact-progress-max` (`var(--sp-100-s48)`), so two steps do not span the whole
screen. The pair keeps its own width — two squircles, two gaps and the bar, 136 at 100 % (160 at
130 / 150 %) — and the row's alignment places it: at the start edge by default, centred or at the
end edge with the alignment modifiers. With 3, 4 or More steps the connectors take the free width and
the cap does not apply.

### Width and alignment

- **Width** — the row takes its host's full width by default. A host may bound it:
  `--progress-steps-compact-min-width` and `--progress-steps-compact-max-width`, set on the instance
  or an ancestor; unset, there is no bound. A min width wider than the host overflows the host,
  so the host keeps room for it.
- **Alignment** — `start` (the bare root), `center` (`.progress-steps-compact--align-center`),
  `end` (`.progress-steps-compact--align-end`). It places the row inside its host when the max width
  leaves free space, and places the two-step pair inside the row. Start and end are logical: in RTL,
  start is the right edge.
- **No scrolling, no wrapping.** Four steps at most; a fifth position is MoreButton. The row's
  floor with `Steps = More` is 272 at 100 % and 332 at 130 / 150 % (paddings, five squircles, gaps,
  the bars at zero); below the floor the row overflows its host rather than scroll.
- Width and alignment are web/host layout parameters; the Figma set stages the full-width row
  only.

### Step content — text or icon

A step's squircle holds either the step's **number** (the Squircle's Text type) or an **icon** (the
Squircle's Icon type) — the StepItem description: "Swap the icon on the exposed Squircle". One
content type per row. The icon is a 24 glyph from `icons/outlined/` in the squircle's slot; it
takes the squircle's content colour, so the step status colours it exactly as it colours a number
(§ 6).

Example — a courier delivery flow, `Steps = More`, Counter "+2":

| Step | Status | Icon (`icons/outlined/`) | Meaning |
|---|---|---|---|
| 1 | Current | `profession/person-courier` | Go to the sender |
| 2 | Upcoming | `actions/info-outline` | Order details |
| 3 | Upcoming | `profession/courier-walk` | Walk to the recipient |
| 4 | Upcoming | `profession/courier` | Hand over the order |

- Web: the bare `.squircle.squircle--s` (no `.squircle--text`) with the inline SVG in
  `.squircle__slot`, `fill="currentColor"`, sized `var(--sp-s24)`.
- **Directional icons mirror in RTL.** The Figma library marks `courier-walk` as RTL; on the web
  the step's SVG takes `.progress-steps-compact__icon--rtl`, which applies `scaleX(-1)` under
  `:dir(rtl)` — the chevron's rule. `courier-walk` is not on the IconContainer § 3.5 list of RTL
  icons — flagged for the icon library.
- The icon is drawn only: the spoken step name comes from the host (§ 10), the same as a number.

### Custom base

`CustomProgressStepsCompact` (`38824:5842`) is the base presets are built on. Its structure is the
main preset's — steps, connectors, MoreButton, the spoken phrase; what a team changes on it:

| 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-compact__step`; an IconContainer step adds `.progress-steps-compact__step--icon-container` (24 glyph, `var(--sp-s24)`) and takes its status colour through its own `color` |
| Paddings | Any SP value | `--progress-steps-compact-padding-block` (`var(--sp-100-s12)`) · `--progress-steps-compact-padding-inline` (`var(--sp-100-s20)`) |
| Divider colour | Any semantic border colour | `--progress-steps-compact-divider` (`var(--border-default)`) |
| ProgressBar style | Any `[ProgressBar] 3.0` style: Primary · Inverse · AlwaysLight · Brand · Accent · Error · Warning | `.progress-steps-compact--progress-{inverse · always-light · brand · accent · error · warning}` (Primary is the bare root); or `--progress-steps-compact-track` / `-fill` |
| Status colours (Squircle steps) | The fill and content of each status | `--progress-steps-compact-{completed · current · upcoming · cancelled}-fill` / `-content` — unset, each is the Squircle state's own value (§ 6) |
| Skeleton shape | Placeholders in the count and form of the team's own steps | Any DS Skeleton shape (`.skeleton`) or Squircle Skeleton inside `__steps`, each with `.progress-steps-compact__step` |
| Steps shown | Any number of StepItems — another limit, a window that follows the current step, or every step of the flow | The host's rendering; the layout is the next row |
| Layout (any number of steps) | **Fill** — the main layout: the connectors share the free width, down to zero; nothing scrolls, so the steps fit the host (40 per step + the gaps) · **Scroll** — the steps scroll sideways, each connector at a fixed length | Fill: no hook. Scroll: `__steps` inside `.progress-steps-compact__scroller` (`tabindex="0"`, `role="group"`, `aria-label`); `--progress-steps-compact-connector-length` (`var(--sp-100-s24)`, the Figma "Divider small (24)") |
| MoreButton | Optional: removed, moved (before `__steps` — the row's start, with `chevron-left`) or replaced by another DS component (a Button S Secondary, for example) | A native button outside `__steps`, after the phrase, with `data-ds-action="open-all-steps"` / `data-ds-target="more-button"`; its touch target per its own spec (a Button S is 40 — the row's `s12` padding gives the separation Button asks for at S) |

- An instance on the custom base carries `data-ds-preset="custom"` (§ 11).
- **Example — any number of steps** (the preview page, Usage): six steps in Fill and ten in
  Scroll, both without MoreButton; the scroller opens with the current step centred (the host's
  job — the web page does it on load).
- **Scroll mode keyboard.** The steps are `aria-hidden` and not focusable, so the scroller itself
  is the keyboard stop that lets the strip scroll (WCAG 2.1.1): `tabindex="0"`, `role="group"`,
  `aria-label="Steps, scrollable"`, focus ring `var(--sp-s2)` in `--text-and-icon-primary` inside its
  edge (`Highlight` in forced colours). The spoken position stays the phrase (§ 10). No scrollbar —
  a step cut by the edge shows the row goes on.
- **Example — MoreButton moved and replaced**: MoreButton at the row's start with `chevron-left`
  and the passed steps behind it; MoreButton replaced by a Button S Secondary "All steps". The
  phrase stays the root's first child, so the position is read before the button.
- **Example — IconContainer steps** (the preview page, Usage): four IconContainers with the courier
  icons — Completed in `TextAndIcon/Success`, Current in `TextAndIcon/Primary`, Upcoming in
  `TextAndIcon/Secondary` — paddings `s8` / `s16`, ProgressBar connectors in style Accent, MoreButton
  unchanged; its own Skeleton is four round 24 placeholders and a Squircle S Skeleton in the
  arrow's place.
- **Example — re-pointed colours** (the preview page, Usage): Squircle steps with Completed content
  in `TextAndIcon/Success` and the Divider in `Border/Success`.
- 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 row with its steps; each step shows its status |
| **Skeleton** | Loading: the flow's steps are not known yet. Four Squircle S in their own Skeleton state (`Skeleton/OnWhite` fill, content hidden, the DS shimmer) joined by the row's connectors — Dividers unchanged (as Figma stages it), ProgressBars in their Skeleton style (the whole bar a `Skeleton/OnWhite` placeholder); no MoreButton, no Counter (Figma `Skeleton = On`, staged with `Steps = 4`, RTL Off / On) |
| **Disabled** | The whole row unavailable. Every squircle — the four steps and MoreButton's — takes the Squircle's own Disabled state in place of its status; Dividers stay unchanged, ProgressBars take their Disabled style; the Counter hides; MoreButton is disabled (owner, 2026-11-10 — not staged in Figma) |

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

- **Skeleton draws four placeholders**, whatever the flow's length — the length is not known
  while loading, and the Figma set stages Skeleton only with `Steps = 4`. In the main preset the
  shape is **static**: it does not follow the number of StepItems the row shows once loaded — 2, 3,
  4 steps or 4 + MoreButton all load from the same four placeholders. A Skeleton that matches the
  real count and form is built on the custom base (§ 3). The row keeps the
  Standard row's height (64) and connectors, so the swap to the real steps does not move the layout.
- Web: the root takes `.progress-steps-compact--skeleton` and `data-ds-state="skeleton"`; each
  step is `squircle squircle--s squircle--skeleton progress-steps-compact__step` with no content
  (the Squircle's own Skeleton state — [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) § 4). The phrase reads
  "Loading". No `aria-busy`: the phrase is the row's live region, and a busy region holds its own
  announcement back (the NumericStepper rule) — the drawn placeholders are already `aria-hidden`
  (§ 10).

- **Disabled** draws no status: the steps read alike, so the position is carried by the spoken
  phrase alone (§ 10). Dividers stay unchanged (`Border/Default`); ProgressBars take the Disabled style (track `Surface/OnWhite`, fill `Surface/ContrastOverlay`), whatever the row's style; their values stay. MoreButton keeps its place (the row's
  layout does not move); its press scale is off and a tap does nothing.
- Web: the root takes `.progress-steps-compact--disabled` and `data-ds-state="disabled"`; each step
  squircle wears `.squircle--disabled` instead of its status class (the Cancelled class too), as
  does MoreButton's squircle; the Counter is hidden by the root modifier; MoreButton carries
  `aria-disabled="true"` — not `disabled`, so it stays reachable (the NumericStepper precedent) —
  and its handler returns early while it is set.

### Step status (StepItem)

| 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 row at most. The content is re-pointed from the Squircle Inverse default (StepItem `38767:1363`, owner 2026-11-10) |
| **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 |

- Cancelled is the Standard Squircle with its content re-pointed to `TextAndIcon/Error` — the
  Squircle has no Error state; the StepItem description in Figma states the re-point.
- The status is drawn by colour alone. Assistive tech hears it in words (§ 10).

### MoreButton

| State | Description |
|---|---|
| **Standard** | The arrow at rest, with or without the Counter |
| **Pressed** | Held: the arrow scales to 95% (`pushButton`, § 5). A motion step — the Figma set stages no Pressed variant |
| **Focused** (web) | Keyboard focus: the DS focus ring on the squircle |
| **Disabled** | With the row's Disabled state only: the squircle Disabled, the Counter hidden, no press scale, 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 |
|---|---|---|---|
| MoreButton press | `pushButton` | `var(--component-push-button-press)` | `transform: scale(0.95)` on the squircle |
| MoreButton release | `pushButton` | `var(--component-push-button-release)` | `transform: scale(1)` |
| MoreButton tap | — | — | Opens the full list — the host's navigation; the component emits the tap |
| Disabled | — | — | No press scale (`:active:not([aria-disabled="true"])`) and no tap; Standard ↔ Disabled is a swap of the squircle states, no transition |
| Step status change | — | — | A swap: the host re-renders the step with its new Squircle State; no transition (Squircle declares none) |
| Current step moves | — | — | The four drawn steps stay steps 1–4; only their statuses swap. Past the fourth step all four are passed and the current step is behind the arrow |
| Skeleton | `Patterns/Shimmer` | `var(--pattern-shimmer)` | The Squircle Skeleton's own sweep on each placeholder; zeroed under reduced motion at the token |
| Skeleton → Standard | — | — | A swap: the host replaces the placeholders with the steps in one frame; the row's height does not change |
| Divider | — | — | Static — no motion |
| ProgressBar value change | `Transforming/State/Default` | `var(--transforming-state-default)` | The fill scales on `transform` (`scaleX`, from the start edge); zeroed under reduced motion |
| Counter change | Indicator's own motion | Indicator § 5 | The count roll is the Indicator's; ProgressStepsCompact adds nothing |

- The press moves the scale alone — no Squircle state defines a pressed fill, the FloatingButton
  precedent.
- Reduced motion: `motion.css` zeroes every `--component-*` token inside
  `prefers-reduced-motion: reduce`, so the press snaps; the component carries no override.

---

## 6. Color tokens

| Element | Token | Notes |
|---|---|---|
| Container | — | Transparent — the row 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` with `--squircle-content: var(--text-and-icon-error)` |
| Divider (`--progress-steps-compact-divider`) | `var(--border-default)` | The default connector; unchanged in Disabled and Skeleton |
| ProgressBar track (`--progress-steps-compact-track`) | `var(--surface-contrast-overlay)` | Style Primary |
| ProgressBar fill (`--progress-steps-compact-fill`) | `var(--background-inverse-primary)` | Style Primary |
| ProgressBar — Disabled | track `var(--surface-on-white)` · fill `var(--surface-contrast-overlay)` | The row's Disabled state, any style |
| ProgressBar — Skeleton | `var(--skeleton-on-white)` · shimmer `var(--skeleton-wave)` | The row's Skeleton state; the fill hidden |
| Disabled — every squircle | `var(--surface-on-white)` · `var(--text-and-icon-disabled)` | `.squircle--disabled`; Dividers unchanged, ProgressBars take the Disabled style; the Counter is hidden |
| Step — Skeleton | `var(--skeleton-on-white)` · shimmer `var(--skeleton-wave)` | `.squircle--skeleton`, content hidden |
| MoreButton | `var(--surface-on-white)` · `var(--text-and-icon-primary)` | `.squircle` (Standard) |
| Counter fill (`--indicator-fill`) | `var(--background-inverse-primary)` | Re-pointed from the Indicator's red default |
| Counter text (`--indicator-content`) | `var(--text-and-icon-inverse-primary)` | **Deviation from Figma** — see below |

Custom base: each status pair is re-pointable through
`--progress-steps-compact-{completed · current · upcoming · cancelled}-fill` / `-content`; unset, the
values above (§ 3).

- **Counter text.** Figma binds the Counter text to `TextAndIcon/AlwaysLight`. In Night
  `Background/InversePrimary` is white and `AlwaysLight` is white too, so the "+N" disappears. The
  web uses `TextAndIcon/InversePrimary` — the Squircle Inverse pair, white on dark in Day, dark on
  white in Night (owner, 2026-11-10). Flagged for a fix in Figma.
- Contrast, content on `Surface/OnWhite` (measured from the primitives):
  - Upcoming `TextAndIcon/Primary` ≈ 15.7:1 Day / ≈ 13.2:1 Night.
  - Current `DoubleColor/Outline-Inverse` on `Background/InversePrimary` ≈ 12.9:1 Day / ≈ 17.8:1 Night.
  - Counter `TextAndIcon/InversePrimary` on `Background/InversePrimary` ≈ 17.0:1 Day / ≈ 17.8:1 Night.
  - Cancelled `TextAndIcon/Error` ≈ 3.5:1 Day / ≈ 3.8:1 Night — under 4.5:1 for 16px text; the
    status is spoken in words (§ 10), so colour is not its only signal for assistive tech.
  - Completed `TextAndIcon/Disabled` ≈ 1.9:1 Day / ≈ 7.6:1 Night — a deliberate dim in Day, the
    Squircle Disabled state; the status is spoken in words.
- ProgressBar styles on the custom base (Figma S-size, track / fill): Inverse
  `var(--surface-dark-overlay)` / `var(--background-always-light)`; AlwaysLight
  `var(--surface-white-overlay)` / `var(--background-always-light)`; Brand · Accent · Error · Warning
  — track `var(--surface-contrast-overlay)`, fill `var(--background-brand)` · `var(--accent-blue2)` ·
  `var(--accent-red2)` · `var(--accent-yellow1)`. Inverse and AlwaysLight belong on dark surfaces,
  Brand on dark only (`color` rule).
- The connectors are supporting graphics. The Divider `Border/Default` on `Background/Primary` is
  low-contrast by design — it joins the steps, it does not carry information.
- The ProgressBar: the fill `Background/InversePrimary` on the track over
  `Background/Primary` reads ≈ 17:1 Day; the empty track alone is low-contrast by design. The value
  is also spoken (§ 10).
- The squircles' `Surface/OnWhite` fill on a `Background/Primary` host is ≈ 1.1:1 Day — the
  Squircle's own contract; on `Surface/OnWhite` hosts the Upcoming steps merge with the ground.

---

## 7. Typography

| Element | Style | Tokens | Scaling |
|---|---|---|---|
| Step icon (Icon type) | — | 24 glyph, `var(--sp-s24)` | `full` — the Squircle S icon (Figma binds `SP/150% ratio`) |
| Step number (Text type) | Body/Main Body — 16/20, Suisse Intl Book | `var(--text-body-main-body-*)` (set by `.squircle--s`) | `full` — grows to 150% (Figma binds `SP/150% ratio`) |
| Counter "+N" | Caption/Caption — 12/16, Suisse Intl Book | `var(--text-caption-caption-*)` (set by `.indicator--m`) | The Indicator's own rule |

The step number is short — one or two digits. Inside the `s40` squircle a two-digit number fits at
100%; the Text slot's own padding (`s4` top/bottom, `s2` left/right) is the Squircle's.

---

## 8. Spacing

Figma binds the paddings and gaps to `SP/100% ratio` (fixed), the squircle box and radius to
`SP/130% ratio` (cap130), the icon to `SP/150% ratio`. The web follows the same families: the
row's own paddings, gap and ProgressBar here, the squircle box and radius in the nested Squircle S
(`var(--sp-130-s40)` / `var(--sp-130-s16)` since Squircle 3.4.4).

| Property | Token | @100% | Figma variable | Notes |
|---|---|---|---|---|
| Container width | — | fills the host | — | 360 in the Figma set; `width: 100%` |
| Container min width | `--progress-steps-compact-min-width` | unset | — | Set by the host; unset = no bound |
| Container max width | `--progress-steps-compact-max-width` | unset | — | Set by the host; unset = no bound. With free space, the alignment places the row |
| Container floor (Steps = More) | — | 272 | — | 332 at 130 / 150 %; the row does not scroll or wrap |
| Container height | — | 64 | — | The squircle + the vertical padding |
| Padding — left / right | `var(--sp-100-s20)` | 20 | `SP/100% ratio/s20` | Custom base: `--progress-steps-compact-padding-inline` |
| Padding — top / bottom | `var(--sp-100-s12)` | 12 | `SP/100% ratio/s12` | Custom base: `--progress-steps-compact-padding-block` |
| Gap between items | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | Between a step and a connector, a connector and a step |
| StepItem / MoreButton box | `var(--sp-130-s40)` | 40 | `SP/130% ratio/s40` | The Squircle S |
| StepItem / MoreButton radius | `var(--sp-130-s16)` | 16 | `SP/130% ratio/s16` | The Squircle S |
| Chevron icon | `var(--sp-s24)` | 24 | `SP/150% ratio/s24` | The Squircle S icon glyph |
| Connector height | `var(--sp-s2)` | 2 | `SP/150% ratio/s2` | Divider and ProgressBar S alike; radius `var(--sp-s16)` (Figma binds the Divider's radius to `SP/130% ratio/s16` — at 2 high the cap does not show) |
| Connector width | — | fills the free width | — | `flex: 1 1 0`, down to zero |
| Connector max width (Steps = 2) | `--progress-steps-compact-progress-max` = `var(--sp-100-s48)` | 48 | — (unbound in Figma) | With two steps the connector's maximum length — the pair stays 136 wide and does not span the screen; the alignment places it |
| Counter offset | `var(--sp-100-s4)` | 4 | — | Outside the top-end corner of MoreButton, both axes |

- The Figma component description names the connector "Divider small (24)"; the variants stretch it
  (owner, 2026-11-10: the variants win). Flagged for the Figma side.
- At 130 / 150% the squircles stop at 52 (Figma, cap130) and the paddings stay put, so the connector
  takes what remains; with `Steps = More` on a 360 host it narrows from 22 to ≈ 7 at 130%.
- Squircle 3.4.4 put S on the ×130 cap; before it the web steps reached 60 at 150 %, and on a
  360 host the fourth step overlapped MoreButton.

---

## 9. Usage context

### When to use ProgressStepsCompact

- A flow of ordered steps the user follows over time — a courier delivery (go to sender, pick up,
  go to recipient, drop off), a multi-stop ride, a document check.
- A tight placement where only the position in the flow matters: a screen header, a card, the top
  of a bottom sheet.
- A long flow where the full list lives one tap away — `Steps = More`: steps 1–4 and MoreButton.
- Steps that read better as pictograms than numbers — the Icon content (§ 3).

### When not to use

- To describe each step — names, addresses, people. That is the full list,
  `[ProgressStepList] 3.0`.
- For a continuous value — a download, a timer. That is a Progress Bar.
- As a stepper the user taps through to move between steps — the steps are not buttons.
- For one step, or for a flow with no order.

### Related components

- [Squircle](https://super-dollop-pzmo65r.pages.github.io/squircle.md) — the StepItem and the MoreButton.
- [Indicator](https://super-dollop-pzmo65r.pages.github.io/indicator.md) — the "+N" Counter.
- [IconContainer](https://super-dollop-pzmo65r.pages.github.io/icon-container.md) — the custom base's alternative StepItem.
- `[ProgressStepList] 3.0` — the full list of steps; a separate component, not yet in the DS.

---

## 10. Accessibility

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

- **One element for the row** (the Figma description): the steps are read as one phrase — the
  position and the current step's name, "Step 2 of 6, Pick up, in progress" — with the cancelled
  steps named. The squircles, numbers, icons and connectors are hidden from assistive tech; a ProgressBar's progress
  after the current step joins the phrase ("…, in progress, 60 percent").
- **MoreButton** is a separate button: "Show all steps", the Counter folded into its label
  ("Show all steps, 2 more"). The Counter is not announced on its own.
- The status is also carried in the row's phrase, so colour is not its only signal for
  assistive tech.
- The position counts the whole flow, not the four drawn steps ("3 of 6 steps completed" when no
  step is in progress); the step's name comes from the host and is dropped when there is none;
  every cancelled step is named after the position, wherever it sits.
- A change of the current step on screen is announced once, politely (Android live region, iOS
  queued announcement, web `role="status"`).
- MoreButton's touch target is `s40`, under the 48dp minimum — the touch region expands to 48
  around it without changing the drawn layout (per-platform technique in the a11y spec).
- **Skeleton:** the row is one element reading "Loading" (localized); the placeholders are hidden,
  nothing is focusable. When the steps land, the same element takes the position phrase, announced
  once. No `aria-busy` on the web — it would hold the live phrase back.
- **Disabled:** the row keeps its phrase — the drawn steps no longer show the position, the phrase
  does. MoreButton stays reachable and refuses activation: TalkBack "Show all steps, Button,
  Disabled", VoiceOver "Show all steps, Dimmed, Button"; the count drops from the label. Web:
  `aria-disabled="true"`, and the click handler returns early — `aria-disabled` alone does not stop
  a native button's `Enter` / `Space` click.
- Reduced motion: the MoreButton press and the skeleton shimmer zero at their tokens.
- Focus order: the row, then MoreButton, in LTR and RTL — the reading order.
- Web focus: an `s2` outline in `var(--text-and-icon-primary)` at radius `s16`. Forced colours: the
  Current step and MoreButton draw system-colour outlines, since the squircle fills are forced away.
- Completed (≈ 1.9:1 Day) and Cancelled (≈ 3.5:1) step content sits under WCAG 1.4.3 for sighted
  users — Owner ruling 2026-11-12: the Figma colours stay; the gap is accepted. (a11y spec, Colour and contrast).

---

## 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-compact` |
| Coverage unit | yes — one root, one DS component instance |
| Tap target model | multiple targets — one internal button (MoreButton); the steps are not targets |
| Actions | `open-all-steps` (MoreButton) |
| Internal targets | `more-button` |
| 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), so a tap event reads where the flow stood |

Attribute model: `data-ds-variant` = the Steps value (`2`, `3`, `4`, `more`; `4` in Skeleton);
`data-ds-state` = `standard`, `skeleton` or `disabled`; the connector kind is not an attribute (no `data-ds-value` while loading; in
Disabled the value stays the current step, and no tap event fires). `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 Indicator are parts of this component, not coverage units of
their own.

```html
<div class="progress-steps-compact"
     data-ds-component="progress-steps-compact" data-ds-component-id="delivery.progress"
     data-ds-variant="more" data-ds-state="standard" data-ds-value="2">
  <p class="visually-hidden" role="status">Step 2 of 6, Pick up, in progress</p>
  <div class="progress-steps-compact__steps" aria-hidden="true">
    <span class="squircle squircle--s squircle--text progress-steps-compact__step squircle--disabled"><span class="squircle__slot">1</span></span>
    <span class="progress-steps-compact__divider"></span>
    <span class="squircle squircle--s squircle--text progress-steps-compact__step squircle--inverse"><span class="squircle__slot">2</span></span>
    <span class="progress-steps-compact__divider"></span>
    <span class="squircle squircle--s squircle--text progress-steps-compact__step"><span class="squircle__slot">3</span></span>
    <span class="progress-steps-compact__divider"></span>
    <span class="squircle squircle--s squircle--text progress-steps-compact__step"><span class="squircle__slot">4</span></span>
    <span class="progress-steps-compact__divider"></span>
  </div>
  <button type="button" class="progress-steps-compact__more" aria-label="Show all steps, 2 more"
          data-ds-action="open-all-steps" data-ds-target="more-button">
    <span class="squircle squircle--s" aria-hidden="true">
      <span class="squircle__slot"><svg class="progress-steps-compact__more-icon" …>chevron-right</svg></span>
    </span>
    <span class="indicator indicator--m indicator--number progress-steps-compact__counter" aria-hidden="true">+2</span>
  </button>
</div>
```

The row's spoken text is the visually hidden paragraph — it stays in the DOM, only its text
changes; the drawn steps are hidden from assistive tech, and MoreButton sits outside that container.
The Skeleton row:

```html
<div class="progress-steps-compact progress-steps-compact--skeleton"
     data-ds-component="progress-steps-compact" data-ds-component-id="delivery.progress"
     data-ds-variant="4" data-ds-state="skeleton">
  <p class="visually-hidden" role="status">Loading</p>
  <div class="progress-steps-compact__steps" aria-hidden="true">
    <span class="squircle squircle--s squircle--skeleton progress-steps-compact__step"></span>
    <span class="progress-steps-compact__divider"></span>
    … four placeholders, three connectors, no MoreButton …
  </div>
</div>
```

When the steps land, the host renders the Standard row in the same root: it removes
`progress-steps-compact--skeleton`, sets `data-ds-state="standard"` and writes the
position into the same phrase paragraph.

The ProgressBar connector replaces each Divider with the bar and its fill; a bar showing progress
carries its value on the bar —
`<span class="progress-steps-compact__progress" style="--progress-steps-compact-value:60;">`;
without it the bar is empty.

An icon step drops `squircle--text` and holds the SVG instead of the number:

```html
<span class="squircle squircle--s progress-steps-compact__step squircle--inverse">
  <span class="squircle__slot"><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">…person-courier, fill="currentColor"…</svg></span>
</span>
<!-- a directional icon (courier-walk) adds class="progress-steps-compact__icon--rtl" on its <svg> -->
```

A cancelled step adds `progress-steps-compact__step--cancelled` to the bare squircle; with two steps
the root takes `progress-steps-compact--steps-2`; a centred or end-aligned row adds
`progress-steps-compact--align-center` / `--align-end`, and a bounded width sets
`--progress-steps-compact-min-width` / `--progress-steps-compact-max-width` (layout, not analytics —
no `data-ds-*` changes). `data-ds-value` is the current step's number, the
last step's number when every step is done, and absent when the flow is not started. The markup is
the preview page's `row` macro (`src/progress-steps-compact.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 (≈ 1.9:1 Day) and Cancelled (≈ 3.5:1) content: the Figma colours stay — the WCAG 1.4.3 / 1.4.1 gap is accepted by the owner (ruling); the spoken status covers screen-reader users. |
| 3.0.0 | 2026-11-10 | Initial spec, from the Figma `[ProgressStepCompact] 3.0` page (`38755:775`, file `uKPcsUAZ9fMW6H9KierbE9`): master `38770:2915`, set `ProgressStepsCompact` `38767:1508` (`Steps = 2 · 3 · 4 · More` × `RTL`), custom base `38824:5842`, building blocks `StepItem` and `MoreButton`. A full-width row, `s20` / `s12` padding and `s4` gap (`SP/100%`); StepItem = Squircle S, its state from the step status (Completed → Disabled, Current → Inverse, Upcoming → Standard, Cancelled → Standard + `TextAndIcon/Error`); a 1dp `Border/Default` line that fills the free width (capped at `s48` with two steps); MoreButton = Squircle S + `chevron-right` + an M Indicator "+N" offset `s4` outside the top-end corner; the first four steps + MoreButton; RTL reverses the row and mirrors the chevron. A11y: the row one element — a `role="status"` phrase counting the whole flow, naming every cancelled step; MoreButton a separate button ("Show all steps, N more") with a 48 touch region around the 40 box. Web build: `shared.css` + page (7 sections, a live current-step demo — steps 1–4 fixed) + `capabilities.json` + registry row + manifest web entry (no node — no bridge carries it). Owner rulings (2026-11-10): the line drawn inside the component; the variants' stretching line over the description's "Divider small (24)"; the steps not tappable; the Counter text `TextAndIcon/InversePrimary` (Figma's `AlwaysLight` is white on white in Night). Squircle 3.4.4 (S/M cap at 130 %) shipped in the same change. Owner fixes (2026-11-10): the line between steps replaced by a ProgressBar S drawn inside the component (Figma `[ProgressBar] 3.0`, page `35163:9013` — the DS ProgressBar is still planned): it shows the progress between two steps (`--progress-steps-compact-value`, 0–100; empty by default), style Primary by default, any style on the custom base (`.progress-steps-compact--progress-{style}`), the Disabled and Skeleton styles in those states; the custom base specified — StepItem swap (IconContainer example), paddings, line colour, per-status colours and the Skeleton shape as web hooks, `data-ds-preset="custom"`; the main preset's Skeleton stated as static (four placeholders whatever loads); the Disabled state (every squircle `.squircle--disabled`, the lines unchanged, the Counter hidden, MoreButton `aria-disabled`; not staged in Figma); the Skeleton state from the Figma `Skeleton = On` variants (`38898:243470` / `38898:243528`, `Steps = 4`) — four Squircle S Skeleton + the lines, no MoreButton, the phrase "Loading" (no `aria-busy` — it would hold the live phrase back); the drawn steps are steps 1–4 — no window following the current step, changeable only in the custom base (supersedes the Figma description's window rule); the Icon step content specified from the Figma courier screen (`person-courier`, `info-outline`, `courier-walk` — mirrored in RTL, `courier`); at most four steps, the fifth position only MoreButton, no scrolling; the two-step line's maximum length made explicit (`--progress-steps-compact-progress-max`, the pair hugs its content); full width by default with an optional min / max width and start / center / end alignment. Then (owner, 2026-11-10): on the custom base any number of StepItems, laid out Fill or Scroll (`.progress-steps-compact__scroller`, a focusable stop; `--progress-steps-compact-connector-length` = `s24`), and MoreButton optional — removed, moved to the start or replaced by another DS component. Earlier: the Current step's content re-pointed to `DoubleColor/Outline-Inverse` (StepItem `38767:1363`; ≈ 12.9:1 Day); two connector kinds — the Divider (`Border/Default`, the default, unchanged in Disabled and Skeleton as Figma stages it; `--progress-steps-compact-divider` on the custom base) and the ProgressBar as the second option. Spec-reviewer, a11y-reviewer and an adversarial contract review folded in. Flags: Completed / Cancelled content under WCAG 1.4.3 (Figma colours); the Figma set key not captured. Neither mobile platform ships it. Descriptions no longer cite Figma mockups — the icon example is a courier delivery flow, not the Figma courier screen. |

---

# ProgressStepsCompact — Accessibility

ProgressStepsCompact tells the user where they are in an ordered flow. Assistive tech gets that as
**one element for the row** (the Figma component description) and, when the flow has more steps
than fit, **one button** — MoreButton — that opens the full list. Nothing else is focusable: the
steps are not controls, and the squircles, numbers, icons, connectors (Dividers or ProgressBars) and the Counter are not
announced on their own.

## The row's phrase

The row is announced as one localized phrase built from the **whole flow**, not from the four
drawn steps. It has two parts: the **position**, then every **cancelled step**, wherever it sits.

### Position — the first rule that matches

| # | Flow state | Position part |
|---|---|---|
| 1 | A step is in progress | "Step 2 of 6, Pick up, in progress" |
| 2 | No step in progress, at least one completed, none upcoming | "All steps completed" — with no cancelled step, "All 6 steps completed" |
| 3 | No step in progress, none completed, at least one upcoming | "6 steps, not started" |
| 4 | No step in progress, some completed, some upcoming | "3 of 6 steps completed" |
| 5 | Every step cancelled | "All 6 steps cancelled" — the cancelled steps are not listed one by one |

### Progress between steps — appended to the position

With the ProgressBar connector, when the bar after the current step shows progress (a value above 0), its value follows
the position: "Step 2 of 6, Pick up, in progress, 60 percent" (the percent word localized). An
empty bar adds nothing, a Divider adds nothing, and the bars behind the current step are not read — the position already
says they are passed. The value is updated in the same phrase, so a change is announced once; a
host that updates the value continuously rounds it (to 10 % steps, for example) so the phrase does
not re-announce on every frame.

### Cancelled steps — appended to the position

Each cancelled step follows the position, in flow order, before or after the current step alike:
"Step 3 of 6, Go to recipient, in progress. Step 2, Pick up, cancelled".

| Example flow | Phrase |
|---|---|
| Step 2 current, none cancelled | "Step 2 of 6, Pick up, in progress" |
| Step 3 current, step 2 cancelled | "Step 3 of 6, Go to recipient, in progress. Step 2, Pick up, cancelled" |
| Step 2 current, step 5 cancelled ahead | "Step 2 of 6, Pick up, in progress. Step 5, Drop off, cancelled" |
| Cancelled at step 2, steps 3–6 upcoming, none current | "1 of 6 steps completed. Step 2, Pick up, cancelled" |
| Every step done but step 4 cancelled | "All steps completed. Step 4, Go to recipient, cancelled" |

- **Position** counts the whole flow ("of 6"), whatever the row draws — the user hears where
  they are, not which four squircles are drawn. Past the fourth step the current step is behind
  the arrow, and the phrase is the only place it is named in the row.
- **The step's name** comes from the host: the drawn step shows a number or an icon, which says
  nothing on its own. It is the same string the full list (`[ProgressStepList] 3.0`) shows for
  that step. An **icon step** (person-courier, courier-walk…) is drawn only, like a number — the
  glyph is not announced. **Without a name** the phrase drops that part and keeps the rest: "Step 2 of 6, in
  progress".
- **Status words** — completed, in progress, not started, cancelled — are localized strings.
  Completed and upcoming steps are not listed one by one — the position covers them.

Label rule: ✅ "Step 2 of 6, Pick up, in progress", ❌ "1, 2, 3, 4, button" (the drawn content read
item by item), ❌ "Step 2" with no total and no name.

## MoreButton

A button, separate from the row, after it in the focus order.

- **Label:** "Show all steps" — with the Counter on, the count joins the label: "Show all steps,
  2 more". The Counter itself is not announced; it sits inside the button's cleared semantics.
- **Action:** opens the full list of steps (the host's navigation).
- **Touch target:** the drawn squircle is `s40`. The hit area reaches 48 × 48, centred on the
  squircle, inside the row's padding (`s12` top and bottom, `s20` at the end edge) — 4 on each side
  fits. The drawn layout — the row's 64 height, the `s4` gap to the last connector — stays as § 8 of the
  main spec gives it, so the hit area is an expanded touch region, not a larger laid-out box. How
  each platform expands it is below; each one is verified on a device.

## Custom base

A preset on `CustomProgressStepsCompact` keeps the same semantics: one element for the row with the
same phrase, MoreButton a separate button. A swapped StepItem (an IconContainer, for example) sits
in the same `aria-hidden` container and is not announced; a re-pointed colour is the team's
contrast check, and a status that matters is shown in more than colour. A team-drawn Skeleton reads
"Loading", like the main preset's.

- **Any number of steps.** The phrase counts the whole flow either way ("Step 6 of 10, Drop off,
  in progress"), so a longer row adds nothing to it.
- **Scroll layout.** The steps stay `aria-hidden`, so the strip needs its own keyboard stop to be
  scrollable (WCAG 2.1.1): the web scroller is `tabindex="0"`, `role="group"`,
  `aria-label="Steps, scrollable"`, with an `s2` focus ring in `--text-and-icon-primary`
  (`Highlight` in forced colours); arrow keys scroll it. The host scrolls the current step into
  view on render and on a change. Native: a horizontal scroll container whose children stay
  cleared by the row's semantics (Android) / ignored (iOS) — swipe navigation does not need to
  scroll it, the phrase already says the position.
- **MoreButton optional.** Removed — the row is the only element. Moved to the start — it stays
  after the phrase in the reading order (the phrase is the root's first child), so the position is
  read first; the focus order is the row, then the button. Replaced by another DS component — it
  keeps a button role, a label that says what it opens ("All steps" / "Show all steps, N more")
  and its own touch-target contract (a Button S is 40, used where the layout separates it — the
  row's `s12` padding does).

## Focus order

Row, then MoreButton, in LTR and RTL — the reading order (on the custom base the DOM order: the phrase first, then the scroller and MoreButton in the order they are drawn). MoreButton is the end-edge element in
both directions (drawn at the right in LTR, at the left in RTL), so no traversal order is set by
hand.

---

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Row | `clearAndSetSemantics { contentDescription = "Step 2 of 6, Pick up, in progress"; liveRegion = LiveRegionMode.Polite }` | — | None — static content, one node | — |
| StepItem / connector | Not nodes — cleared by the row's `clearAndSetSemantics` | — | — | — |
| MoreButton | `clearAndSetSemantics { contentDescription = "Show all steps, 2 more"; role = Role.Button; onClick(…) }` (without the count when the Counter is off) — the Counter's "+2" text is cleared with it | — | `Role.Button` | TalkBack's own "Double-tap to activate" |
| Counter | Not a node — cleared by MoreButton | — | — | — |

- **Hit area — the trap:** `Modifier.minimumInteractiveComponentSize()` reserves 48dp in
  *measurement* — the row grows to 72 and the `s4` gap to the last connector collapses. The 40 box keeps
  its laid-out size and the touch region is expanded around it instead (a touch-bounds extension on
  the clickable, not a larger box). If the DS3 Compose base is Material, note that
  `minimumInteractiveComponentSize` is governed by `LocalMinimumInteractiveComponentSize` and is the
  wrong tool here for the same reason.
- **Live region:** the row is a polite live region, so a change of its description while the screen
  is open is read once. The node stays in the tree across updates — only its description changes.
  Whether TalkBack also speaks the node when it first appears, and whether the first data arriving
  after a loading phase is announced, is **verified on a device** before release; if the first
  appearance is spoken, the live region is set only after the first render.

### Edge states

- **Disabled:** the row keeps its phrase — the position is still true and the drawn steps no
  longer show it. MoreButton stays a node, reachable by swipe, with `disabled()` in its semantics:
  TalkBack reads "Show all steps, Button, Disabled" (the count drops with the hidden Counter) and
  ignores the double-tap.
- **Skeleton / loading:** the Skeleton row is one element too — `clearAndSetSemantics {
  contentDescription = "Loading" }` (localized); the four placeholders are not nodes and nothing is
  focusable (no MoreButton). It is the same node as the Standard row, with the same
  `liveRegion = LiveRegionMode.Polite` and the same first-render rule as above (set after the first
  render if the first appearance would be spoken). When the steps land, only its
  `contentDescription` changes to the position phrase — the live region reads it once. The placeholders' shimmer stops under reduced motion.
- **Error / Cancelled:** a cancelled step is named in the row's phrase ("Step 2, Pick up,
  cancelled"); its red content is not the only signal.
- **No MoreButton** (`Steps = 2 · 3 · 4`): the row is the only node.
- **Counter off:** MoreButton's label drops the count ("Show all steps").
- **Text scaling:** the step number (Main Body) scales to 150%, the DS maximum, inside a box that
  stops at 130% — a one- or two-digit number still fits. The phrase is unaffected.
- **RTL:** the phrase and the focus order are unchanged; the chevron and RTL-marked step icons
  mirror.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Row | `accessibilityElement(children: .ignore)` + `accessibilityLabel("Step 2 of 6, Pick up, in progress")` | — | `.staticText` (SwiftUI `.isStaticText`) | — |
| StepItem / connector | Not elements — ignored by the row | — | — | — |
| MoreButton | `accessibilityElement(children: .ignore)` + "Show all steps, 2 more" (without the count when the Counter is off) | — | `.button` (SwiftUI `.isButton`) | "Opens the full list of steps." |
| Counter | Not an element — ignored by MoreButton | — | — | — |

- **Hit area:** `.contentShape` only shapes hit-testing inside the view's own bounds, and a 48 × 48
  `.frame` changes the layout. The 40 box keeps its size and the touch region grows around it —
  UIKit: `point(inside:with:)` returning `true` for the 48 × 48 rect; SwiftUI: a transparent 48 × 48
  overlay placed with an offset that does not take part in the layout. At least the 44 × 44 HIG
  minimum. Verified on a device.
- **A change of the current step** while the screen is open: VoiceOver has no live region on an
  element, so the host posts the new phrase as an announcement. A plain
  `UIAccessibility.post(notification: .announcement, argument:)` **interrupts** current speech; to
  queue it politely the argument is an attributed string with
  `.accessibilitySpeechQueueAnnouncement = true` (SwiftUI: `AccessibilityNotification.Announcement`
  with an `AttributedString` carrying the same attribute).

### Edge states

- **Dimmed / Disabled:** the row keeps its label. MoreButton stays an element with the
  `.notEnabled` trait — VoiceOver reads "Show all steps, Dimmed, Button" and ignores the
  double-tap; no hint.
- **Skeleton / loading:** the row is one element labelled "Loading", with the placeholders
  ignored (`children: .ignore`) and `accessibilityHidden` on the shapes; when the steps land the
  host posts the new phrase as a queued announcement, as for a step change.
- **Error / Cancelled:** as on Android — named in the row's label.
- **No MoreButton / Counter off:** as on Android.
- **Dynamic Type:** as on Android — the number scales to 150% inside a box capped at 130%.
- **RTL:** as on Android.

---

## Web preview (reference)

- **The row's phrase** is a `.visually-hidden` paragraph with `role="status"`; the drawn steps sit
  in a container with `aria-hidden="true"`. The paragraph 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>` — naming a generic element is prohibited (ARIA 1.2).
- **MoreButton** is a native `<button type="button">` with `aria-label` ("Show all steps, 2 more");
  its squircle, chevron and Indicator are `aria-hidden="true"`. The 48 hit area is a transparent
  `::before` layer, inset 4 outside the squircle, so the drawn size stays `s40`. The row and its
  host clip no overflow on that side — a clipping ancestor would cut the layer back to 40.
- **Focus:** only MoreButton takes keyboard focus. The ring is an `s2` outline in
  `var(--text-and-icon-primary)` inside the squircle's edge at radius `s16`, `:focus-visible` only —
  the FloatingButton ring. An `outline`, not a `box-shadow`, so it survives forced colours. Enter
  and Space activate it — native button behaviour.
- **Forced colours** (Windows High Contrast): every squircle fill is forced to Canvas. The Current
  step draws an `s2` outline in the system text colour, so the position stays visible, and MoreButton
  draws a 1px outline in `ButtonText`, so it reads as a button and not a bare chevron. The Divider and the ProgressBar track take
  `GrayText`, the ProgressBar fill the system text colour.
- **Disabled:** the root carries `.progress-steps-compact--disabled`; the phrase is unchanged.
  MoreButton takes `aria-disabled="true"` (not `disabled` — that would drop it from the tab order)
  and its label drops the count ("Show all steps"); the click handler returns early while it is set
  — `aria-disabled` does not stop a native button's `Enter` / `Space` click. Forced colours: its
  outline turns `GrayText`.
- **Skeleton:** the phrase paragraph reads "Loading". No `aria-busy` anywhere on the row — the
  paragraph is the live region, and a busy ancestor would hold its announcement back (the
  NumericStepper rule); the placeholders are already hidden in the `aria-hidden` container;
  the placeholders sit in the `aria-hidden` container and take no focus. When the steps land the
  host renders the steps and writes the position into the same paragraph — announced once, politely. Forced colours: the skeleton fill becomes Canvas, so each
  placeholder keeps a 1px `GrayText` outline.
- **Reduced motion:** the MoreButton press zeroes at the token (`--component-push-button-*`), and
  so does the skeleton shimmer (`--pattern-shimmer`); nothing else moves.

### Colour and contrast

Measured from the primitives (`tokens/generated/primitives.css`).

| 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 |
| MoreButton chevron | `--text-and-icon-primary` on `--surface-on-white` | ≈ 15.7:1 Day / ≈ 13.2:1 Night |
| Counter "+N" | `--text-and-icon-inverse-primary` on `--background-inverse-primary` | ≈ 17.0:1 Day / ≈ 17.8:1 Night (Figma's `AlwaysLight` would be ≈ 1:1 in Night — see the main spec § 6) |
| ProgressBar fill | `--background-inverse-primary` on `--background-primary` | ≈ 17:1 Day — the value is also spoken |
| Divider | `--border-default` on `--background-primary` | Low by design — a joining line, no information |
| ProgressBar track (empty) | `--surface-contrast-overlay` on `--background-primary` | Low by design — an empty bar carries no information |
| Squircle edge | `--surface-on-white` on `--background-primary` | ≈ 1.1:1 Day — the Squircle's own contract |

**Owner ruling (2026-11-12) — WCAG 1.4.3.** The Completed (1.9:1 Day) and Cancelled (3.5:1) numbers carry
meaning, and a completed step is not an inactive control, so the disabled exemption is a stretch.
The spoken phrase covers screen-reader users, not sighted low-vision users. The colours are the
Figma contract (Squircle Disabled; `TextAndIcon/Error`), kept as drawn — the owner accepts the gap.

---

## Machine contract — `specs/components/progress-steps-compact/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-compact",
  "name": "ProgressStepsCompact",
  "version": "3.0.2",
  "description": "A full-width row of up to four steps of an ordered flow (a courier delivery), each a Squircle S whose state follows the step status, joined by connectors — a Divider (Border/Default, the default) or a ProgressBar S (empty, or showing the progress between two steps); a trailing MoreButton (chevron + an M Indicator \"+N\") opens the full list when the flow has more steps — the drawn steps are steps 1–4, the fifth position is MoreButton, and the row does not scroll or shift. A step holds its number or an icon. Full width by default, with an optional min / max width and start / center / end alignment. The row is one accessibility element, MoreButton a separate button; the steps are not tappable.",
  "files": {
    "spec": "specs/components/progress-steps-compact/progress-steps-compact.md",
    "a11y": "specs/components/progress-steps-compact/progress-steps-compact-a11y.md",
    "preview": "src/progress-steps-compact.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "[ProgressStepCompact] 3.0 — new component file (page 38755:775). Set 38767:1508 on page 38755:775 — RTL (Off · On) × Steps (2 · 3 · 4 · More) × Skeleton (Off · On) — ten staged: Skeleton = On only with Steps = 4 (38898:243470 LTR, 38898:243528 RTL) (variant names read from the set, 2026-11-10); the master [ProgressStepCompact] 3.0 is 38770:2915, the custom base CustomProgressStepsCompact 38824:5842. Building blocks: StepItem 38767:1381 (Status = Completed · Current · Upcoming · Cancelled), MoreButton 38786:3606 (RTL; Counter boolean). The component key is not captured yet — the desktop MCP read gives node-ids only; read it with the Plugin API before the first bridge binding.",
    "fileKey": "uKPcsUAZ9fMW6H9KierbE9",
    "capturedAt": "2026-11-10"
  },
  "root": {
    "class": "progress-steps-compact",
    "dataDsComponent": "progress-steps-compact"
  },
  "anatomy": {
    "phrase": {
      "class": "visually-hidden",
      "notes": "A <p class=\"visually-hidden\" role=\"status\"> — the first child of the root, before __steps: the row's only accessible text (\"Step 2 of 6, Pick up, in progress\"). 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-compact__steps",
      "notes": "The second child of the root, aria-hidden: every step and every ProgressBar lives inside it, in flow order. MoreButton is NOT inside it — a focusable button inside an aria-hidden container is unreachable."
    },
    "step": {
      "class": "progress-steps-compact__step",
      "count": "2–4",
      "notes": "A Squircle S wearing its own classes plus .progress-steps-compact__step: squircle squircle--s progress-steps-compact__step (+ squircle--text for a number; an icon step is the bare type, the inline SVG in .squircle__slot at s24, fill currentColor, and a directional glyph — courier-walk — carries .progress-steps-compact__icon--rtl). The status classes are the stepStatus axis. Box and radius var(--sp-130-s40) / var(--sp-130-s16) from Squircle 3.4.4."
    },
    "scroller": {
      "class": "progress-steps-compact__scroller",
      "notes": "Custom base, layout = scroll only: a wrapper around __steps that scrolls sideways (overflow-x auto, no scrollbar). The one keyboard stop of the strip — tabindex=\"0\", role=\"group\", aria-label=\"Steps, scrollable\" — because the steps are aria-hidden; focus ring var(--sp-s2) in --text-and-icon-primary inside its edge (Highlight in forced colours). Inside it each connector takes --progress-steps-compact-connector-length (unset = var(--sp-100-s24)). The host scrolls the current step into view.",
      "required": false
    },
    "divider": {
      "class": "progress-steps-compact__divider",
      "notes": "The default connector between neighbouring steps inside __steps — the Figma Divider: var(--sp-s2) high, radius var(--sp-s16), flex: 1 1 0 (the free width, down to zero), background --progress-steps-compact-divider (unset = --border-default). Static; unchanged in the Disabled and Skeleton states (as Figma stages Skeleton). One connector kind per row — the connector axis.",
      "count": "steps − 1, + 1 with steps = more (connector = divider)"
    },
    "progress": {
      "class": "progress-steps-compact__progress",
      "notes": "The second connector kind (connector = progress-bar, owner 2026-11-10): a ProgressBar S between neighbouring steps inside __steps — Figma [ProgressBar] 3.0 S-size (page 35163:9013), drawn here because the DS ProgressBar is still planned (owner, 2026-11-10). Track var(--sp-s2) high, radius var(--sp-s16), flex: 1 1 0 (the free width, down to zero); its child .progress-steps-compact__progress-fill fills from the start edge to --progress-steps-compact-value (0–100, unitless, on the bar; unset = the empty bar) via transform: scaleX, transition var(--transforming-state-default). Style Primary by default (track --surface-contrast-overlay, fill --background-inverse-primary). With steps = more one trailing bar follows the fourth step.",
      "count": "steps − 1, + 1 with steps = more (connector = progress-bar)"
    },
    "more": {
      "class": "progress-steps-compact__more",
      "onlyWhen": {
        "steps": [
          "more"
        ]
      },
      "notes": "A native <button type=\"button\"> — the root's last child, a sibling AFTER __steps (outside the aria-hidden container). Inside: a Squircle S Standard (aria-hidden) whose slot holds chevron-right at s24 with .progress-steps-compact__more-icon (mirrored in RTL), then the optional Counter. aria-label \"Show all steps\" + \", N more\" when the Counter is on; data-ds-action=open-all-steps, data-ds-target=more-button. Opens the full list (the host's navigation). pushButton press (scale only); a transparent ::before extends the hit area to 48 without changing the layout; :focus-visible draws an s2 outline in --text-and-icon-primary inside the squircle's edge. Custom base: optional — removed, moved before __steps (the row's start, chevron-left) or replaced by another DS component that stays a native button outside __steps with data-ds-action=open-all-steps / data-ds-target=more-button."
    },
    "counter": {
      "class": "progress-steps-compact__counter",
      "optional": true,
      "onlyWhen": {
        "steps": [
          "more"
        ],
        "counter": [
          true
        ]
      },
      "notes": "An Indicator M Number (indicator indicator--m indicator--number) \"+N\" — N = the flow's length − 4, the steps after the fourth — s4 (var(--sp-100-s4)) outside MoreButton's top-end corner; --indicator-fill --background-inverse-primary, --indicator-content --text-and-icon-inverse-primary (Figma's AlwaysLight is white on white in Night — owner, 2026-11-10). aria-hidden; the count joins MoreButton's label. direction: ltr + unicode-bidi: isolate, so \"+N\" does not read \"N+\" in RTL."
    }
  },
  "axes": {
    "steps": {
      "title": "Steps",
      "type": "enum",
      "values": [
        "2",
        "3",
        "4",
        "more"
      ],
      "default": "2",
      "css": {
        "modifier": ".progress-steps-compact--steps-2",
        "mechanism": "the number of .progress-steps-compact__step children; more = four steps + .progress-steps-compact__more; .progress-steps-compact--steps-2 on the root if and only if steps = 2 — it gives the connector a maximum length (--progress-steps-compact-progress-max = var(--sp-100-s48)) and lets the pair hug its content (136 at 100 %, 160 at 130 / 150 %), so two steps do not span the row; the alignment axis places the pair — not with 3, 4 or more"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Steps",
        "values": {
          "2": "2",
          "3": "3",
          "4": "4",
          "more": "More"
        }
      },
      "constraints": [
        "At most four steps are drawn, and with steps = more they are the flow's steps 1–4 — no window that follows the current step, no scrolling, no wrapping; the fifth position is only ever MoreButton. Past the fourth step all four drawn steps are passed and the current step is behind the arrow (the phrase still names it). A different limit or a following window only in the custom base CustomProgressStepsCompact (owner, 2026-11-10; supersedes the Figma description's window rule).",
        "At a 360 host the ProgressBar is 48 (2, capped — the pair 136 wide), 92 (3), ≈ 45.3 (4), 22 (more); at 130 / 150 % the squircles stop at 52 and the bar takes the rest (≈ 7 with more)."
      ]
    },
    "align": {
      "title": "Alignment",
      "type": "enum",
      "values": [
        "start",
        "center",
        "end"
      ],
      "default": "start",
      "css": {
        "mechanism": "start = the bare root · center = .progress-steps-compact--align-center · end = .progress-steps-compact--align-end (margin-inline auto + justify-content, logical — start is the right edge in RTL)"
      },
      "figma": {
        "kind": "layout",
        "notes": "Not a property of the set — the Figma set stages the full-width row only; alignment is the instance's placement in its parent."
      },
      "constraints": [
        "Places the row in its host when maxWidth leaves free space, and the two-step pair inside the row. With 3, 4 or more steps at full width it has no visible effect."
      ]
    },
    "minWidth": {
      "title": "Min width",
      "type": "text",
      "default": "unset",
      "customizable": "any host length; unset = no bound",
      "css": {
        "customProperty": "--progress-steps-compact-min-width"
      },
      "figma": {
        "kind": "layout",
        "notes": "The instance's min width in auto layout — not a property of the set."
      },
      "constraints": [
        "Set on the instance or an ancestor. A min width wider than the host overflows the host — the row does not scroll."
      ]
    },
    "maxWidth": {
      "title": "Max width",
      "type": "text",
      "default": "unset",
      "customizable": "any host length; unset = no bound (the row fills its host)",
      "css": {
        "customProperty": "--progress-steps-compact-max-width"
      },
      "figma": {
        "kind": "layout",
        "notes": "The instance's max width in auto layout — not a property of the set."
      },
      "constraints": [
        "Set on the instance or an ancestor; with free space left, the align axis places the row."
      ]
    },
    "paddingBlock": {
      "title": "Padding — top / bottom (custom base)",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-100-s12",
      "css": {
        "customProperty": "--progress-steps-compact-padding-block"
      },
      "figma": {
        "kind": "none",
        "notes": "CustomProgressStepsCompact 38824:5842 — paddings unlocked."
      },
      "constraints": [
        "Custom base only (data-ds-preset=\"custom\"); any SP token."
      ]
    },
    "paddingInline": {
      "title": "Padding — left / right (custom base)",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-100-s20",
      "css": {
        "customProperty": "--progress-steps-compact-padding-inline"
      },
      "figma": {
        "kind": "none",
        "notes": "CustomProgressStepsCompact 38824:5842 — paddings unlocked."
      },
      "constraints": [
        "Custom base only; any SP token."
      ]
    },
    "connector": {
      "title": "Connector",
      "type": "enum",
      "values": [
        "divider",
        "progress-bar"
      ],
      "default": "divider",
      "css": {
        "mechanism": "divider: a .progress-steps-compact__divider between steps (--border-default) · progress-bar: a .progress-steps-compact__progress with its .progress-steps-compact__progress-fill child. Same box (s2 high, the free width, the two-step cap), so the kind does not move the layout. Not a root modifier — the element itself."
      },
      "figma": {
        "kind": "none",
        "notes": "Figma stages the Divider (Border/Default) only; the ProgressBar option is an owner ruling (2026-11-10), its look from [ProgressBar] 3.0 S-size (page 35163:9013)."
      },
      "constraints": [
        "One kind per row.",
        "progressStyle and progressValue apply to progress-bar only; a Divider keeps --border-default in every state (custom base: --progress-steps-compact-divider)."
      ]
    },
    "progressStyle": {
      "title": "ProgressBar style",
      "type": "enum",
      "values": [
        "primary",
        "inverse",
        "always-light",
        "brand",
        "accent",
        "error",
        "warning"
      ],
      "default": "primary",
      "css": {
        "modifierTemplate": ".progress-steps-compact--progress-{value}",
        "mechanism": "primary = the root default (or the explicit .progress-steps-compact--progress-primary); the others re-point --progress-steps-compact-track / --progress-steps-compact-fill (Figma S-size values). The row's Disabled and Skeleton states override the style."
      },
      "figma": {
        "kind": "none",
        "notes": "The Style property of the nested [ProgressBar] 3.0 S (page 35163:9013); unlocked on CustomProgressStepsCompact 38824:5842."
      },
      "constraints": [
        "Connector = progress-bar only.",
        "Custom base only — the main preset is Primary.",
        "Inverse and AlwaysLight on dark surfaces; Brand on dark only (colour rule)."
      ]
    },
    "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-compact-{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": "CustomProgressStepsCompact 38824:5842 — status colours unlocked."
      },
      "constraints": [
        "Custom base only. Squircle steps; a swapped StepItem colours itself."
      ]
    },
    "stepItem": {
      "title": "StepItem component (custom base)",
      "type": "enum",
      "values": [
        "squircle",
        "icon-container",
        "other-ds-component"
      ],
      "default": "squircle",
      "css": {
        "mechanism": "squircle: the main preset · icon-container: .progress-steps-compact__step.progress-steps-compact__step--icon-container with the SVG (s24) and its own color · other: any DS component carrying .progress-steps-compact__step"
      },
      "figma": {
        "kind": "none",
        "notes": "CustomProgressStepsCompact 38824:5842 — StepItems swappable (IconContainer named on the Figma page note)."
      },
      "constraints": [
        "Custom base only; inside the aria-hidden __steps; one component type per row."
      ]
    },
    "layout": {
      "title": "Layout for any number of steps (custom base)",
      "type": "enum",
      "values": [
        "fill",
        "scroll"
      ],
      "default": "fill",
      "css": {
        "mechanism": "fill: the main layout — connectors flex: 1 1 0, down to zero; the steps fit the host (40 per step + gaps), nothing scrolls · scroll: __steps inside .progress-steps-compact__scroller; connectors at --progress-steps-compact-connector-length (var(--sp-100-s24))",
        "customProperty": "--progress-steps-compact-connector-length"
      },
      "figma": {
        "kind": "none",
        "notes": "CustomProgressStepsCompact 38824:5842 — any number of StepItems (owner, 2026-11-10)."
      },
      "constraints": [
        "Custom base only — the main preset draws at most four steps and never scrolls.",
        "Scroll: the scroller is focusable (tabindex=0, role=group, aria-label) so a keyboard can scroll the aria-hidden steps; the host keeps the current step in view."
      ]
    },
    "moreButton": {
      "title": "MoreButton (custom base)",
      "type": "enum",
      "values": [
        "end",
        "start",
        "none",
        "replaced"
      ],
      "default": "end",
      "css": {
        "mechanism": "end: .progress-steps-compact__more after __steps (the main preset) · start: the same button before __steps, after the phrase, with chevron-left · none: absent · replaced: another DS component (e.g. a Button S Secondary) in its place"
      },
      "figma": {
        "kind": "none",
        "notes": "CustomProgressStepsCompact 38824:5842 — MoreButton optional (owner, 2026-11-10)."
      },
      "constraints": [
        "Custom base only.",
        "Whatever opens the full list is a native button outside __steps with data-ds-action=open-all-steps and data-ds-target=more-button, a label naming what it opens and its own touch-target contract (Button S 40, separated by the row's s12 padding); the phrase stays the root's first child."
      ]
    },
    "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 re-pointed to --double-color-outline-inverse, Figma StepItem 38767:1363) · upcoming the bare .squircle · cancelled the bare .squircle + .progress-steps-compact__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 — not a property of the ProgressStepsCompact set."
      },
      "constraints": [
        "Per step, not per row. At most one current step per row; none when the flow is not started or complete.",
        "Steps before the current one are completed or cancelled; steps after it upcoming or cancelled."
      ]
    },
    "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 (s24) in .squircle__slot"
      },
      "figma": {
        "kind": "none",
        "notes": "The nested Squircle's own Type (Text | Icon) — the set stages numbers; the StepItem description: \"Swap the icon on the exposed Squircle\"; the courier screen 38839:238596 stages icons."
      },
      "constraints": [
        "One content type per row.",
        "The number or icon is drawn only — the spoken step name comes from the host."
      ],
      "notes": "text: the step number · icon: a 24 glyph from icons/outlined/ — a courier delivery flow uses person-courier, info-outline, courier-walk (RTL-marked in Figma, mirrored via .progress-steps-compact__icon--rtl), courier."
    },
    "progressValue": {
      "title": "Progress between steps",
      "type": "number",
      "min": 0,
      "max": 100,
      "default": 0,
      "css": {
        "customProperty": "--progress-steps-compact-value"
      },
      "figma": {
        "kind": "none",
        "notes": "The Value of the nested [ProgressBar] 3.0 (0–100 %)."
      },
      "constraints": [
        "Connector = progress-bar only.",
        "Per ProgressBar, set on the bar; unset or 0 = the empty bar (the track alone). A host that does not show progress leaves every bar empty.",
        "The value of the bar after the current step joins the phrase (\", 60 percent\"); bars behind it are not read."
      ]
    },
    "counter": {
      "title": "Counter",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "the .progress-steps-compact__counter element present or absent inside MoreButton"
      },
      "figma": {
        "kind": "none",
        "notes": "A BOOLEAN property (Counter) on the nested MoreButton 38786:3606 — not a property of the ProgressStepsCompact set."
      },
      "constraints": [
        "Only with steps = more. Data-driven: \"+N\", N = the flow's length − 4 (≥ 1 with steps = more), and \", N more\" joins MoreButton's aria-label. Off (the Figma boolean) the arrow stands alone with the label \"Show all steps\" — a custom-base choice; the standard component keeps it on."
      ]
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "dir=\"rtl\" on the root or an ancestor: the row reverses (flex follows the direction), the chevron and any .progress-steps-compact__icon--rtl step icon mirror under :dir(rtl), the Counter stays on the top-end corner and keeps \"+N\""
      },
      "figma": {
        "kind": "variant-property",
        "property": "RTL",
        "values": {
          "false": "Off",
          "true": "On"
        }
      }
    },
    "skeleton": {
      "title": "Skeleton",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".progress-steps-compact--skeleton",
        "mechanism": "the root modifier + data-ds-state=\"skeleton\"; four .squircle--skeleton steps (no content) and three connectors inside __steps — Dividers unchanged, ProgressBars in their Skeleton style (the bar a --skeleton-on-white placeholder with the shimmer, the fill hidden); no MoreButton; the phrase reads \"Loading\" — no aria-busy (a busy ancestor holds the role=\"status\" phrase back)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Skeleton",
        "values": {
          "false": "Off",
          "true": "On"
        }
      },
      "constraints": [
        "Main preset: static — four Squircle S placeholders whatever the row shows once loaded (2, 3, 4 steps or 4 + MoreButton); it does not track the real number of StepItems. Staged in Figma with Steps = 4 only. No MoreButton, no Counter, no data-ds-value. A shape matching the real count and form is the custom base's: any DS Skeleton / Squircle Skeleton placeholders, each with .progress-steps-compact__step.",
        "When the steps land the host renders the Standard row in the same root: drops the modifier, sets data-ds-state=\"standard\", writes the position into the same phrase paragraph."
      ]
    }
  },
  "states": {
    "default": [
      "standard",
      "skeleton",
      "disabled"
    ],
    "moreButton": [
      "standard",
      "pressed",
      "focus-visible",
      "disabled"
    ]
  },
  "analytics": {
    "dataDsComponent": "progress-steps-compact",
    "actions": [
      "open-all-steps"
    ],
    "targets": [
      "more-button"
    ],
    "valueAttr": "data-ds-value",
    "notes": "Coverage unit. data-ds-variant = the Steps value (2, 3, 4, more), data-ds-state = standard, skeleton or disabled, data-ds-preset absent on the main preset, \"custom\" on an instance built on the custom base. data-ds-value = the current step's number; N (the last step) when every step is done; omitted when the flow is not started. The steps are not targets; the nested Squircles and Indicator are parts, not coverage units."
  },
  "rtl": {
    "supported": true,
    "notes": "Step 1 at the right edge, MoreButton at the left; the chevron and RTL-marked step icons (courier-walk) mirror, the Counter stays top-end and reads \"+N\"; numbers and other icons do not mirror; the phrase and the focus order are unchanged."
  },
  "constraints": [
    "Markup order: root > p.visually-hidden[role=status] (the phrase) > div.progress-steps-compact__steps[aria-hidden] (steps and connectors) > button.progress-steps-compact__more (steps = more only). Every step carries .progress-steps-compact__step, every connector .progress-steps-compact__divider or .progress-steps-compact__progress (with a .progress-steps-compact__progress-fill child), the Counter .progress-steps-compact__counter, the chevron .progress-steps-compact__more-icon — without them the forced-colours outline, the Counter's offset and colours, and the RTL mirror are lost.",
    "Paddings s20 left/right and s12 top/bottom, gap s4, the s48 cap and the Counter offset on the --sp-100-* family (Figma SP/100% ratio) — they keep their size while the squircles scale to 130 %.",
    "Width: width: 100% by default, bounded only by --progress-steps-compact-min-width / -max-width. No scrolling and no wrapping: the floor with steps = more is 272 at 100 % and 332 at 130 / 150 % (paddings, five squircles, gaps, connectors at zero); below it the row overflows its host.",
    "Disabled (owner, 2026-11-10; not in Figma): .progress-steps-compact--disabled + data-ds-state=\"disabled\" on the root; every squircle (steps and MoreButton's) wears .squircle--disabled in place of its status class; Dividers unchanged, ProgressBars take the Disabled style (track --surface-on-white, fill --surface-contrast-overlay); the Counter is hidden; MoreButton aria-disabled=\"true\" (reachable, no press, no tap — the handler guards), label without the count.",
    "The row is one accessibility element; its phrase counts the whole flow (\"Step 2 of 6, Pick up, in progress\"; \"3 of 6 steps completed\"; \"6 steps, not started\"; \"All 6 steps completed\"), names every cancelled step after the position, and drops a missing step name. MoreButton follows the row in the reading order, LTR and RTL.",
    "No ancestor of MoreButton clips overflow on its side (overflow: hidden) — the ::before 48 hit area extends s4 outside the s40 squircle.",
    "The step's name comes from the host — the drawn number or icon says nothing on its own.",
    "Completed (≈ 1.9:1 Day) and Cancelled (≈ 3.5:1) content is under WCAG 1.4.3 — the Figma colours, flagged for the owner.",
    "Custom base (CustomProgressStepsCompact, data-ds-preset=\"custom\"): any number of StepItems, laid out fill or scroll (layout axis, .progress-steps-compact__scroller); MoreButton optional — removed, moved to the start or replaced (moreButton axis); the StepItem may be another DS component (IconContainer: .progress-steps-compact__step--icon-container); paddings, the Divider colour (--progress-steps-compact-divider), the ProgressBar style and the per-status colours re-point through the paddingBlock / paddingInline / progressStyle / statusColours hooks; the Skeleton shape is the team's. The structure — steps, connectors, the phrase — is unchanged.",
    "Neither mobile platform ships it — no manifest node binding until a platform bridge carries [ProgressStepCompact] 3.0."
  ]
}
```
