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

---

# ChipsChoice · Oymyakon DS 3

> A selectable pill: a tap turns it on or off — several at once with Multi Choice, one in its group with
> Single Choice — to pick options, filter, or set a parameter in place.

**Version:** 3.0.5 · **Status:** Ready · **Figma:** `[ChipsChoice] 3.0` (main components file `7vdl5YkZFDWvh9QvSmydsH`, page `10568:21887`; sets `.M-ChipsChoice` `9991:11068` (key `59c9d109…`) and `.S-ChipsChoice` `10008:23290` (key `f1e9a322…`) — both private, dot-prefixed; the control glyph set `.↳🔵Controls` `10158:9884`; the start-slot set `.↳🔴Slot#1` `10086:29717`, shared with ChipsAction; Specification frame `22969:17025`. The states, the press and the "Select" accessibility blocks are on the 3.0 Chips Specification frame on the `[ChipsAction] 3.1` page (`6390:17466`); the rename ChipsItem → ChipsChoice and the Unselected fill change are in that page's `Update 3.1` frame `22838:14180`)

---

## 1. Description

ChipsChoice is a pill-shaped control that holds a short title and shows whether it is chosen. A tap
changes the choice; the chip itself carries no other action (Figma: "used alone or to create a chip
group that allows selecting one or multiple values").

- **Multi Choice** (Figma "Multi Choice (Checkbox)") — works like a checkbox: each chip turns on and
  off on its own, and any number of chips in a row can be on — additional services for an order
  ("One Mover", "Two Movers", "Special Cargo").
- **Single Choice** (Figma "Single Choice (Radio button)") — works like a radio button: one chip in
  its group is on, and choosing another turns the previous one off — a type of cargo, a pickup time
  ("10–20 min", "Up to 1 hour").

A chosen chip is **Active**: its fill and its border change and the control glyph in its end slot
shows a check or a dot. The Figma name "Active" is kept — the state is the chip's selection.

The 3.1 update renamed ChipsItem to ChipsChoice, split the action chip off as
[ChipsAction](https://super-dollop-pzmo65r.pages.github.io/chips-action.md), and changed the unselected fill from
`Surface/OnGrey` to `Background/Primary` for both. A row of chips is a
[ChipsArea](https://super-dollop-pzmo65r.pages.github.io/chips-area.md) (Figma `[ChipsArea] 3.0`), not part of this spec; a Single
Choice group needs one — the chip's side of its grouping and keyboard model is described here for the
web.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | `DsChipsChoice` (DS3) | Figma 3.0 | `init(isSelected:size:preset:title:leadingSlot:trailingSlot:)` — `isSelected` is a `Binding<Bool>`; `preset` `.multiSelect` / `.singleSelect`; `DsChipsChoiceSize.m` / `.s`; `subtitle`, `isTrailingSlotHidden`, `.disabled(true)`, `.isLoading(true)` (manifest bindings) |
| Android | `DsChip.Choice` (`core.compose.ds.v3.chip`) | Figma 3.0 | `selected`, `type` `DsChip.ChoiceType.MultiSelect` / `.SingleSelect` (sets the glyph and the semantic role), `size` `DsChip.Size.M` / `.S`, `enabled`, `title`, `subtitle`, `startSlot`; Skeleton is ambient through `DsSkeletonState` (manifest bindings) |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.chips-choice`) | 3.0.0 | Reference implementation, this repo; preview page `src/chips-choice.njk` |

Both platforms also ship a **Filter** preset — a choice chip with no control glyph (iOS
`DsChipsChoicePreset.filter`, Android `DsChip.ChoiceType.Filter`). The Figma set has no such value; on
the web it is a ChipsChoice with the control turned off.

---

## 2. Anatomy

```
ChipsChoice (.chips-choice)                      button · hug × min s48 (M) / s40 (S) · min width s64
└── Content (.chips-choice__content)             the row that scales on press · gap s0
    ├── Start slot (.chips-choice__slot--start)      optional, off · s24 · Icon / Number Indicator
    ├── Text container (.chips-choice__text)         one column · s4 left / right padding
    │   ├── Title (.chips-choice__title)             Main Body · one line
    │   └── Subtitle (.chips-choice__subtitle)       optional, off · Caption
    └── Control (.chips-choice__control)             on by default · s24 · the checkbox / radio glyph
```

| Part | Required | Description |
|---|---|---|
| **Container** | Required | A pill: hugs its content, minimum width `s64`, minimum height `s48` (M) / `s40` (S), padding `s12` at the start and end, radius full, a `s2` border drawn inside the box and outside the layout. The whole container is the one tap target |
| **Content** | Required | The row of the slot, the text and the control, centred. On press it scales as one piece; the container stays still |
| **Start slot** | Optional, off | Figma `🔴Slot#1` — the slot set shared with ChipsAction: an icon (any DS icon, s24) or a [Number Indicator](https://super-dollop-pzmo65r.pages.github.io/indicator.md) L. Figma also offers an Avatar — no web build |
| **Text container** | Required | Figma `TextContainer`: ONE container holding the Title and the Subtitle as a centred column — `s4` padding left and right, `s0` top and bottom, `s0` between the two lines. Its side padding is the gap to a slot, the control or the chip's edge |
| **Title** | Required | One line in `Body/Main Body`. The chip hugs it |
| **Subtitle** | Optional, off | A second line in `Caption/Caption` |
| **Control** | Optional, on | Figma `🔵Slot#2` holding the private `.↳🔵Controls` glyph, `s24`: Multi Choice — an empty rounded square ("checkbox-unselected"), a check when Active ("checkbox-selected"); Single Choice — an empty circle ("radio-unselected"), a circle with a dot when Active ("radio-selected"). The glyph is a picture of the state, not a second control: the chip is the checkbox or the radio |

- There is no Counter and no corner Indicator — those are ChipsAction's.
- With the control off the chip still changes its fill and border when Active — the platforms' Filter
  preset.

### RTL layout

`RTL = true` mirrors the row: the control goes to the left edge and the start slot to the right. The
glyphs keep their orientation — the Figma RTL variants use the same glyph, so the check is not mirrored. On the web the root's direction (`dir="rtl"` on it or an
ancestor) mirrors the row.

---

## 3. Variants and sizes

Two sizes, published in Figma as two component sets — the web presets.

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Size** (preset) | `m` · `s` | `m` | The bare `.chips-choice` is M (min height `s48`); `.chips-choice--s` is S (min height `s40`). `data-ds-preset="m"` / `"s"` |
| **Chips Type** | `multi` · `single` | `multi` (Figma "Multi Choice (Checkbox)") | `role="checkbox"` with the checkbox glyphs; `role="radio"` with the radio glyphs, inside a `role="radiogroup"` |
| **State** | `standard` · `active` · `skeleton` | `standard` (Figma Default) | `aria-checked="false"` / `"true"`; `.chips-choice--skeleton` |
| **Disabled** | `false` · `true` | `false` | The native `disabled` attribute — on its own or with `aria-checked="true"`; `aria-disabled="true"` renders the same (a disabled ChipsArea uses it) |
| **Start slot** | `false` · `true` | `false` | The `.chips-choice__slot--start` element |
| **Subtitle** | `false` · `true` | `false` | The `.chips-choice__subtitle` element |
| **Control** | `true` · `false` | `true` | The `.chips-choice__control` element |
| **RTL** | `false` · `true` | `false` | `dir="rtl"` on the root or an ancestor |

M and S differ only in the minimum height. In Figma Disabled is its own property, not a State value:
a disabled chip can be Standard or Active.

---

## 4. States

| State | What shows |
|---|---|
| **Standard** | `Background/Primary` fill, a `Border/Default` border, the Title and Subtitle in `TextAndIcon/Primary`, the empty glyph in `TextAndIcon/Secondary` |
| **Active** | `Surface/OnWhite` fill, a `Border/Active` border, the check or the dot in `TextAndIcon/Primary` |
| **Disabled** | The Standard fill and border; the Title, the Subtitle and the glyph in `TextAndIcon/Disabled`; no press, no change |
| **Disabled + Active** | The Standard fill and border — not the Active ones; the check or the dot in `TextAndIcon/Disabled` keeps showing the choice |
| **Skeleton** | An empty pill — no slot, no text, no control, no border — `s124` wide, at the size's height, in `Skeleton/OnWhite` with the DS shimmer |

Interaction on an enabled chip, Standard or Active (the 3.0 Specification frame's "Rest · Hover/Pressed ·
Focus (only Web)" columns):

| Interaction | What shows |
|---|---|
| **Hover** (web, pointer) | The DS pressed overlay (`--shadow-pressed`) over the fill |
| **Pressed** | The same overlay, and the content scales to 95% (`pushButton`); the pill keeps its size |
| **Focused** (web) | The border turns `Border/Inform` |

- The Figma set calls the first state "Default"; the DS name is **Standard**.
- Active is told apart by the border and the glyph, not by the fill alone — the Active fill is close
  to the Standard one (≈ 1.1:1 Day). On focus the border turns `Border/Inform`, so the glyph is what
  survives; with the control off (the platforms' Filter preset) a focused Active chip is told apart by
  the fill alone, and a disabled Active chip not at all — it takes the Standard fill and border; the
  state is still announced — accepted (owner, 2026-11-12).
- A disabled chip keeps its place and its fill — a row of chips never reflows when one is disabled.

---

## 5. Animation and behavior

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

| Event | Pattern | Token | Notes |
|---|---|---|---|
| Press | `pushButton` | `var(--component-push-button-press)` | The content scales 100% → 95% and the pressed overlay comes in, together; the pill keeps its size |
| Release | `pushButton` | `var(--component-push-button-release)` | 95% → 100%, the overlay out |
| Hover in / out (web) | `pushButton` | `var(--component-push-button-release)` | The overlay alone, no scale |
| Choice on / off — the fill | `Transforming/State/Default` | `var(--transforming-state-default)` | `Background/Primary` ↔ `Surface/OnWhite`, the Checkbox precedent |
| Choice on / off — the border | `pushButton` | `var(--component-push-button-release)` | The border is one `box-shadow` with the overlay, so it shares that transition |
| Choice on / off — the glyph | — | — | The empty glyph and the check / dot swap in the same frame |
| Focus (web) | `pushButton` | `var(--component-push-button-release)` | The border colour eases with the overlay — the same `box-shadow` |
| Skeleton shimmer | `Patterns/Shimmer` | `var(--pattern-shimmer)` | The DS sweep across the pill; reversed in RTL |
| Skeleton → content | — | — | The host swaps the skeleton for the chip; no transition of its own |

- A tap on a Multi Choice chip toggles it. A tap on a Single Choice chip turns it on and the chip that
  was on in its group off; a tap on the chip that is already on changes nothing.
- A disabled chip and a Skeleton do not react to hover, press or a tap.
- Reduced motion: `--component-push-button-*`, `--transforming-state-default` and `--pattern-shimmer`
  are zeroed at the token — the overlay, the 95% scale and the choice apply at once, with no
  transition, and the skeleton holds still. No override of its own.

---

## 6. Color tokens

| Element | Token | Notes |
|---|---|---|
| Fill — Standard | `var(--background-primary)` | `--chips-choice-fill`. A default — an instance may re-point it to any DS colour. Was `Surface/OnGrey` before 3.1 |
| Fill — Active | `var(--surface-on-white)` | `--chips-choice-fill-active`. A default |
| Border — Standard | `var(--border-default)` | `--chips-choice-border`. A default |
| Border — Active | `var(--border-active)` | `--chips-choice-border-active`. A default |
| Start-slot icon | `var(--text-and-icon-primary)` | `currentColor` from the root. A default |
| Title, Subtitle | `var(--text-and-icon-primary)` | The root colour. A default |
| Glyph — Standard | `var(--text-and-icon-secondary)` | The empty square or circle |
| Glyph — Active | `var(--text-and-icon-primary)` | The check or the dot |
| Disabled content | `var(--text-and-icon-disabled)` | The Title, the Subtitle, the start-slot icon and the glyph — Standard or Active |
| Pressed / hover overlay | `var(--shadow-pressed)` | Over the fill, under the border |
| Focus border | `var(--border-inform)` | Web |
| Skeleton fill | `var(--skeleton-on-white)` | |
| Skeleton wave | `var(--skeleton-wave)` | The shimmer's highlight |

The colour contract is four custom properties on the root — `--chips-choice-fill`,
`--chips-choice-border`, `--chips-choice-fill-active`, `--chips-choice-border-active` — plus the root
`color`. A disabled chip uses the Standard pair whatever its choice.

- Contrast: the Title ≈ 17.8:1 Day / ≈ 17.0:1 Night on the Standard fill, ≈ 15.7:1 / ≈ 13.2:1 on the
  Active one; the empty glyph ≈ 5.25:1 / ≈ 7.92:1 (above the 3:1 non-text floor).
- With the control on, the Active border and the glyph tell a chosen chip apart — the border ≈ 17.8:1 /
  ≈ 17.0:1 against the page, the glyph changes shape; the Active fill alone is ≈ 1.1:1 / ≈ 1.3:1 against
  the Standard one.
- The focus border ≈ 4.8:1 / ≈ 4.7:1 on the Standard fill and ≈ 4.2:1 / ≈ 3.65:1 on the Active one.
- `TextAndIcon/Disabled` is below the contrast floor on purpose — a disabled control is exempt.
- A re-pointed fill keeps the Title and the Subtitle at 4.5:1 or more, the glyph and the border at 3:1.
- Forced colours (Windows High Contrast) drop `box-shadow`: the web draws an outline in system colours
  instead — `ButtonBorder`, `Highlight` on focus, `GrayText` when disabled; an Active chip's outline
  is drawn thicker so the choice survives.
- **Figma flags:** the sets are private (dot-prefixed) though both platforms ship the component and the
  `Update 3.1` frame says it is used alone; the Active Multi glyph is the **deprecated**
  `checkbox-selected` icon (`icons/outlined/deprecated/`); the Skeleton variants bind `Surface/OnWhite`
  (the web uses `Skeleton/OnWhite`, the same primitive); the 3.0 Specification frame writes the pressed
  overlay as "#000000, opacity 8%" — a detached copy; the components use the style
  `State/PressedStateOverlay` (10%) = `--shadow-pressed`, the one press token.

---

## 7. Typography

| Element | Style | Tokens | Scaling |
|---|---|---|---|
| Title | Body/Main Body — 16/20, Suisse Intl Book | `var(--text-body-main-body-*)` | `full` |
| Subtitle | Caption/Caption — 12/16, Suisse Intl Book | `var(--text-caption-caption-*)` | `full` |

- The Title and the Subtitle are one line each; the chip hugs the longer of the two. Figma stages no
  overflow; the web caps the chip at its container's width and ends an over-long Title or Subtitle in
  an ellipsis — the accessible name stays whole.
- The Subtitle is Caption inside an interactive control — Figma's choice; the typography rules keep
  Caption for non-interactive text (§ 11), with one exception since 2026-11-12 (owner): the secondary line of a control, whose name the title carries — this Subtitle, as on ChipsAction.

---

## 8. Spacing

The container is ChipsAction's: Figma binds the same tokens to the same places.

| Property | Token | @100% | Notes |
|---|---|---|---|
| Width | — | hugs | |
| Min width | `var(--sp-s64)` | 64 | |
| Min height — M | `var(--sp-s48)` | 48 | |
| Min height — S | `var(--sp-s40)` | 40 | |
| Padding — start / end | `var(--sp-s12)` | 12 | |
| Padding — top / bottom | `var(--sp-s0)` | 0 | The content is centred on the min height |
| Radius | `var(--sp-s96)` | full | A full pill at every size |
| Border | `var(--sp-s2)` | 2 | Drawn inside the box, outside the layout flow — it adds nothing to the size. The web draws it as an inset ring (`box-shadow`) |
| Gap — slot ↔ text ↔ control | `var(--sp-s0)` | 0 | The Text container's own side padding makes the gaps |
| Text container padding — left / right | `var(--sp-s4)` | 4 | |
| Text container padding — top / bottom | `var(--sp-s0)` | 0 | |
| Gap — Title ↔ Subtitle | `var(--sp-s0)` | 0 | |
| Start slot | `var(--sp-s24)` | 24 × 24 minimum | Hugs its content; content centred |
| Control | `var(--sp-s24)` | 24 × 24 | The glyph |
| Skeleton width | `var(--sp-s124)` | 124 | Fixed — there is no title to hug |

At 100%: a "Title" chip with its control is 12 + 4 + 32 + 4 + 24 + 12 = 88 wide (the Figma 88), and
48 high (M) or 40 (S).

---

## 9. Usage context

### When to use ChipsChoice

- Several options that can be added together — services for an order ("One Mover", "Two Movers",
  "Special Cargo") — Multi Choice.
- One value out of a short set shown in full — a pickup time, a type of cargo — Single Choice.
- Filtering content in place, a few options at a time — the platforms' Filter preset (the control
  off).

### When not to use

- To run an action or open an input — [ChipsAction](https://super-dollop-pzmo65r.pages.github.io/chips-action.md).
- For a long list of options, or options that need a description — a list of
  [Cells](https://super-dollop-pzmo65r.pages.github.io/cell.md) with a [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) or a
  [RadioButton](https://super-dollop-pzmo65r.pages.github.io/radiobutton.md).
- To switch between views of the same content — a [SegmentedControl](https://super-dollop-pzmo65r.pages.github.io/segmented-control.md).
- For one on/off setting on its own — a Checkbox (or a Switcher on mobile).

### Related components

- [ChipsAction](https://super-dollop-pzmo65r.pages.github.io/chips-action.md) — the same pill as an action; shares the start-slot set.
- [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) and [RadioButton](https://super-dollop-pzmo65r.pages.github.io/radiobutton.md) — the semantics a Multi and a Single Choice chip follow.
- [Indicator](https://super-dollop-pzmo65r.pages.github.io/indicator.md) — the Number Indicator L a start slot may hold.
- [ChipsArea](https://super-dollop-pzmo65r.pages.github.io/chips-area.md) — the row and the group: the Filter, Multi Choice and Single Choice presets.

---

## 10. Accessibility

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

- **The chip is the checkbox or the radio** — one target, one focus stop; the glyph is a picture of
  the state and is not announced on its own.
- **Multi Choice**: TalkBack — "[Title], [Subtitle], Not checked / Checked, Checkbox"; VoiceOver — a
  button with the selected trait ("Selected").
- **Single Choice**: in a radio group — TalkBack "Checked, 2 of 4, Radio button"; VoiceOver a button
  with the selected trait; on the web one Tab stop for the group (on an enabled option), arrow keys move
  the choice and follow the visual direction in RTL.
- **Disabled**: TalkBack "Disabled", VoiceOver "Dimmed"; a disabled Active chip still says it is
  checked.
- **Skeleton**: not announced, not focusable.
- **Keyboard (web)**: Space toggles a Multi Choice chip and checks a Single Choice one; Enter does
  nothing — the Checkbox and RadioButton precedent.
- **Touch target**: M is 48 high; S is 40 — under the 44 floor of the typography rules, as Button S
  and ChipsAction S — an accepted exception (owner, 2026-11-12; `typography-rules.md` § 12): WCAG 2.5.8 AA met at every mode, 52 at 130 %.
- **Focus** (web): the border turns `Border/Inform`; forced colours draw a system-colour outline.

---

## 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` | `chips-choice` |
| Coverage unit | yes — one root, one DS component instance |
| Tap target model | root — the whole chip is one `<button>` |
| Actions | `toggle` (Multi Choice), `select` (Single Choice) |
| Internal targets | none |
| Emits value | yes — `data-ds-state` (`standard` / `active`) is the choice, updated in the same handler as `aria-checked` |

Attribute model: `data-ds-preset` = the size (`m`, `s`); `data-ds-variant` = the Chips Type (`multi`,
`single`); `data-ds-state` = `standard`, `active` or `skeleton` (a disabled chip keeps its state and
carries `disabled`). RTL is the reading direction, not a state.

```html
<!-- Multi Choice — the chip is a checkbox; the row is a group named by the screen's label -->
<p id="options">Options</p>   <!-- the screen's own label; not part of the component -->
<div role="group" aria-labelledby="options">
<button type="button" class="chips-choice" role="checkbox" aria-checked="true"
        data-ds-component="chips-choice" data-ds-component-id="order.options.two-movers"
        data-ds-preset="m" data-ds-variant="multi" data-ds-state="active"
        data-ds-action="toggle">
  <span class="chips-choice__content">
    <span class="chips-choice__slot chips-choice__slot--start" aria-hidden="true"><svg …>an icon</svg></span>
    <span class="chips-choice__text">
      <span class="chips-choice__title">Two Movers</span>
      <span class="visually-hidden">, </span><span class="chips-choice__subtitle">Up to 2 hours</span>
    </span>
    <span class="chips-choice__control" aria-hidden="true">
      <svg class="chips-choice__glyph chips-choice__glyph--off" …>checkbox-unselected</svg>
      <svg class="chips-choice__glyph chips-choice__glyph--on" …>checkbox-selected</svg>
    </span>
  </span>
</button>
</div>

<!-- Single Choice — a radio in a radio group; one Tab stop for the group -->
<p id="pickup-time">Pickup time</p>   <!-- the screen's own label; not part of the component -->
<div role="radiogroup" aria-labelledby="pickup-time">
  <button type="button" class="chips-choice" role="radio" aria-checked="true" tabindex="0"
          data-ds-component="chips-choice" data-ds-component-id="delivery.pickup.fast"
          data-ds-preset="m" data-ds-variant="single" data-ds-state="active" data-ds-action="select">…</button>
  <button type="button" class="chips-choice" role="radio" aria-checked="false" tabindex="-1"
          data-ds-component="chips-choice" data-ds-component-id="delivery.pickup.hour"
          data-ds-preset="m" data-ds-variant="single" data-ds-state="standard" data-ds-action="select">…</button>
</div>
```

The start slot takes both classes — `.chips-choice__slot` carries the box, `--start` names it. The chip
takes its name from its own text — the Title, then the Subtitle after a `.visually-hidden` ", "
separator. In a Single Choice group the one `tabindex="0"` sits on the checked option when it is
enabled, otherwise on the first enabled option, and is re-synced whenever `disabled` or `aria-checked`
changes. Both glyphs sit in the control; `aria-checked` shows one of them through CSS, so the
handler flips `aria-checked` and `data-ds-state` and nothing else. Disabled is the native `disabled`
attribute; Skeleton is `.chips-choice--skeleton` with `aria-hidden="true"`, `inert` and no content.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.5 | 2026-11-12 | `aria-disabled="true"` renders as Disabled — drawn and announced the same, refusing the activation, focusable — for a disabled ChipsArea (3.0.1). |
| 3.0.4 | 2026-11-12 | Owner rulings: the Filter preset's focus-time cue gap is accepted (the state is announced); the checkbox / radio semantics stand over the Figma preview's toggle button. Status Draft → Ready (owner). |
| 3.0.3 | 2026-11-12 | The Caption Subtitle inside the control is accepted (owner ruling; `typography-rules.md` § 11.2). The Filter non-colour cue and the checkbox-vs-Toggle announcement stay open. |
| 3.0.2 | 2026-11-12 | S at 40 recorded as an accepted exception to the 44 touch-target floor, as Button S and ChipsAction S (owner ruling; `typography-rules.md` § 12). The Filter non-colour cue, the Caption subtitle and the checkbox-vs-Toggle announcement stay open. |
| 3.0.1 | 2026-11-03 | ChipsArea shipped on the web: § 1 and § 9 link [ChipsArea](https://super-dollop-pzmo65r.pages.github.io/chips-area.md) in place of "Figma only". § 6 corrected: with the control off a disabled Active chip is not told apart at all (it takes the Standard fill and border), as the a11y spec says — not "by the fill alone". |
| 3.0.0 | 2026-10-29 | Initial spec, from the Figma `[ChipsChoice] 3.0` page in the main components file (page `10568:21887`): the private `.M-ChipsChoice` (`9991:11068`) and `.S-ChipsChoice` (`10008:23290`) sets, the `.↳🔵Controls` glyph set (`10158:9884`), its Specification frame (`22969:17025`), and — for the states, the press and the "Select" accessibility — the 3.0 Chips Specification frame and the `Update 3.1` frame on the `[ChipsAction] 3.1` page. A selectable pill on the ChipsAction container (min `s64` × `s48` / `s40`, `s12` sides, radius full, a `s2` inside border outside the layout). Chips Type Multi Choice (a checkbox) / Single Choice (a radio in a group); Title in Main Body, optional Caption Subtitle, optional start slot, the control glyph on by default (checkbox-unselected / checkbox-selected, radio-unselected / radio-selected). States Standard (`Background/Primary`, `Border/Default`, glyph `TextAndIcon/Secondary`) · Active (`Surface/OnWhite`, `Border/Active`, glyph `TextAndIcon/Primary`) · Skeleton (`s124`); Disabled as its own property — the Standard fill and border, content `TextAndIcon/Disabled`, an Active chip's check or dot kept. Press = `pushButton`; the choice's fill eases on `Transforming/State/Default`. Web: `role="checkbox"` / `role="radio"` in a `radiogroup` with a roving tab stop, `aria-checked` drives the glyph and the colours. Figma flags: the private sets; the deprecated `checkbox-selected` icon; Skeleton bound to `Surface/OnWhite`; the frame's detached 8% overlay copy (the style is 10%); the platforms' Filter preset is not in the set. Spec-reviewer (0 critical, 1 high, 4 medium, 4 low) and a11y-reviewer (0 critical, 2 high, 5 medium, 4 low) passes folded in: a focused checked chip kept no indicator in forced colours (a heavier `:focus-visible` selector beat the outline — removed); a Single Choice group keeps one Tab stop on an enabled option and clears a disabled option's choice when another is picked; modified arrow keys left to the browser; Enter on a radio does nothing; RTL arrows recorded; the Multi row is a named group; the start slot's two classes and `valueAttr` documented; `onlyWhen` for Skeleton; the Filter preset's weak state cue and the VoiceOver hint wording flagged for the owner. |

---

# ChipsChoice — Accessibility

ChipsChoice **is** the checkbox or the radio button: the whole chip is one target and one focus stop,
and its state — checked or not — is the control's own value. The glyph in its end slot (an empty
square or circle, a check or a dot) is a picture of that state and is never announced on its own.

- **Multi Choice** follows the [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md): each chip is checked or not,
  on its own.
- **Single Choice** follows the [RadioButton](https://super-dollop-pzmo65r.pages.github.io/radiobutton.md): the chips are
  options of one group, and the group is what makes them a set — its name, the position in the set,
  one Tab stop on the web.

The name is the chip's text — **[Title], [Subtitle]**. The screen supplies the group's name (the
label next to the row: "Options", "Pickup time"), and a word for a Number Indicator in the start slot
("2 new").

| Visible | Announced (TalkBack) |
|---|---|
| Two Movers ✓ (Multi, Active) | "Two Movers, Checked, Checkbox, Double-tap to toggle" |
| One Mover ☐ (Multi) | "One Mover, Not checked, Checkbox, Double-tap to toggle" |
| 10–20 min ◉ (Single, Active, 1 of 2) | "10–20 min, Checked, 1 of 2, Radio button" |
| Up to 1 hour ○ (Single, 2 of 2) | "Up to 1 hour, Not checked, 2 of 2, Radio button, Double-tap to select" |

**Figma's own preview differs.** The 3.0 Chips Specification frame (the "Select" blocks) voices a
choice chip as "Not selected, Title, Subtitle, Button, Double-tap to select" on TalkBack and gives it
the Toggle trait on VoiceOver. The DS follows its Checkbox and RadioButton contracts instead — a chip
that works like a checkbox is announced as one — and Android's `DsChip.Choice` sets the semantic role
from its `type`. The DS semantics stand (owner, 2026-11-12); the Figma preview is to be updated.

---

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Chip — Multi Choice | "[Title], [Subtitle]" — the text | "Checked" / "Not checked" — `toggleable(value, role = Role.Checkbox)` | Checkbox | "Double-tap to toggle." |
| Chip — Single Choice | "[Title], [Subtitle]" | "Checked" / "Not checked", "[n] of [m]" — `selectable(selected, role = Role.RadioButton)` in a `selectableGroup()` | Radio button | "Double-tap to select." — none when checked |
| Group (Single Choice) | The screen's label for the row | — | — (`selectableGroup()`) | — |
| Start-slot icon, the glyph | — | — | Decorative — not nodes | — |
| Number Indicator in the start slot | Its meaning from the screen, appended to the label | — | — | — |

Voiced previews:

- "Two Movers, Checked, Checkbox, Double-tap to toggle."
- "Up to 1 hour, Not checked, 2 of 2, Radio button, Double-tap to select."

### Edge states

- **Disabled:** `enabled = false`. TalkBack keeps the chip in swipe navigation, reads its value and
  appends "Disabled", without the hint: "Two Movers, Checked, Checkbox, Disabled." A double-tap does
  nothing.
- **After a tap:** the new value is read on the focused chip ("Checked"); on a Single Choice chip the
  previous option turns off silently — it is not announced.
- **Whole group disabled** (Single Choice): every option reads its value and "Disabled"; the group
  label is still read.
- **Skeleton:** not announced, not focusable — `clearAndSetSemantics {}` on the shape; the loading
  container announces once, politely.
- **RTL:** the row mirrors; the name keeps the language's reading order.
- **Text scaling:** the Title and the Subtitle scale to 150%, the DS maximum.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Chip — Multi Choice | "[Title], [Subtitle]" — `accessibilityLabel` | — | `.button` + `.isSelected` when checked | "Double tap to toggle setting." |
| Chip — Single Choice | "[Title], [Subtitle]" | — | `.button` + `.isSelected` when checked | "Double tap to select." — none when checked |
| Group (Single Choice) | The screen's label for the row | — | Container (`accessibilityElement(children: .contain)`) | — |
| Start-slot icon, the glyph | — | — | Decorative — `accessibilityHidden(true)` | — |
| Number Indicator in the start slot | Its meaning from the screen, appended to the label | — | — | — |

Voiced previews:

- "Two Movers, Selected, Button. Double tap to toggle setting."
- "Up to 1 hour, Button. Double tap to select."

iOS has no native checkbox or radio trait: the state is the **selected** trait, as on the DS Checkbox
and RadioButton. The hints keep those components' wording ("Double tap to toggle setting.", "Double tap
to select.") for parity — Apple's idiom prefers no gesture in a hint (ChipsAction sets none); flagged
for the owner. VoiceOver announces no position in the set for a group of buttons — the "1 of 2" is
TalkBack's and the web's.

### Edge states

- **Disabled:** `.notEnabled` — VoiceOver appends "Dimmed"; a disabled Active chip reads "Selected"
  then "Dimmed". It stays in navigation; a double-tap does nothing; the hint is cleared.
- **After a tap:** the selected trait changes on the focused chip and VoiceOver reads it; nothing else
  is announced.
- **Whole group disabled** (Single Choice): every option reads "Selected" where it is, then "Dimmed".
- **Skeleton:** `accessibilityHidden(true)` on the shape (`.isLoading(true)` alone does not hide it);
  the loading container announces once.
- **RTL / Dynamic Type:** as on Android.

---

## Web preview (reference)

- **Multi Choice:** a `<button type="button" role="checkbox" aria-checked="true|false">`. The role and
  `aria-checked` are required — a `<button>` alone announces as a button and exposes no value.
  `type="button"` keeps a chip inside a form from submitting it.
- **Multi Choice row:** `role="group"` with `aria-labelledby` pointing at the screen's label
  ("Options"), so the chips are heard as one set.
- **Single Choice:** `<button type="button" role="radio" aria-checked>` options inside a
  `role="radiogroup"` with `aria-labelledby` pointing at the screen's label. Without the group a screen
  reader has no set to announce.
- **Name:** the chip's own text — the Title, then the Subtitle after a `.visually-hidden` ", " — so
  the two lines never run together. The start slot and the control are `aria-hidden="true"`; a Number
  Indicator's meaning is a `.visually-hidden` phrase after the text.
- **One handler:** a tap flips `aria-checked` and `data-ds-state` together — the CSS shows the glyph
  and the colours from `aria-checked`, so the accessible value, the picture and the analytics value
  never drift apart.
- **Keyboard — Multi Choice:** Tab reaches each chip; **Space** toggles; **Enter** does nothing — the
  Checkbox precedent. A `<button>` synthesizes a click on Enter, so the handler swallows a click that
  came from Enter (the Checkbox trap); a `type="button"` chip never submits a form either way.
- **Keyboard — Single Choice:** the group is **one Tab stop** — roving `tabindex`: `0` on the checked
  option when it is enabled, otherwise on the first enabled option, `-1` on the rest; no stop when every
  option is disabled. Re-sync it whenever an option's `disabled` or `aria-checked` changes — a disabled
  checked option holding the only `0` leaves the group unreachable. The arrow keys move the focus and the
  choice together, wrap at the ends and skip disabled options; picking an option clears the choice on
  every other one, a disabled one too. **In RTL** the arrows follow the visual direction (ArrowRight
  moves to the previous option) — a deliberate divergence from the native radio, which does not flip.
  Arrows with Alt, Ctrl, Meta or Shift are left to the browser. **Space** checks the focused option;
  **Enter** does nothing, with the same guard.
- **Disabled:** the native `disabled` attribute — out of the Tab order and skipped by the arrows,
  readable in browse mode, its `aria-checked` still announced. Inside a disabled ChipsArea the chip
  carries `aria-disabled="true"` instead — focusable, drawn and announced the same, refusing the
  activation (ChipsArea 3.0.1).
- **Skeleton:** `aria-hidden="true"`, `inert`, no content; the loading region around the row
  announces.
- **Focus:** `:focus-visible` turns the `s2` ring `var(--border-inform)` — ≈ 4.8:1 against the
  Standard fill, ≈ 4.2:1 against the Active one in Day (≈ 4.7:1 / ≈ 3.65:1 Night).
- **Forced colours:** Windows High Contrast drops `box-shadow`; a real outline in system colours
  stands in — `ButtonBorder`, thicker when checked, `Highlight` on focus, `GrayText` when disabled. The
  glyph keeps showing the choice.
- **Long text:** the chip is capped at its container's width and an over-long Title or Subtitle ends
  in an ellipsis on the screen; the accessible name stays whole.
- **Target size:** M is `s48` high, above the `s44` floor of the typography rules (§ 12, iOS 44 pt).
  S is `s40` — under that floor and Material's 48 dp, as Button S and ChipsAction S; recorded as
  an accepted exception (owner, 2026-11-12; `typography-rules.md` § 12): WCAG 2.5.8 AA met at every mode, 52 at 130 %; the drawn size is the target.
  The SP modes lift S to 52 at 130% and 60 at 150%. Every chip is at least `s64` wide.
- **Not colour alone (control on):** a chosen chip is told apart by its `Border/Active` border and the
  changed glyph, not by the fill (the two fills are ≈ 1.1:1) — WCAG 1.4.1. On focus the border turns
  `Border/Inform`, so the glyph is the cue that survives.
- **Control off (the platforms' Filter preset):** with no glyph, a focused Active chip and a focused
  Standard one differ only by the fill (≈ 1.1:1), and a disabled Active chip looks like a disabled
  Standard one. The state is still announced. Accepted (owner, 2026-11-12) — the focus ring stays as on ChipsAction.
- **Reduced motion:** the overlay, the 95% scale and the choice apply at once, and the skeleton holds
  still — all zeroed at the token.

---

## Checklist

- [ ] The chip is the checkbox or the radio — one target, one focus stop
- [ ] Multi: "Checked" / "Not checked", Checkbox (TalkBack); the selected trait (VoiceOver)
- [ ] Single: inside a named radio group; the position in the set is announced (TalkBack, web); one Tab
      stop on the web, on an enabled option
- [ ] The glyph and the start-slot icon are hidden from assistive tech
- [ ] Space toggles a Multi chip; Enter does not
- [ ] Disabled: "Disabled" / "Dimmed", the value still read, no action
- [ ] Skeleton is hidden and not focusable

---

## Machine contract — `specs/components/chips-choice/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": "chips-choice",
  "name": "ChipsChoice",
  "version": "3.0.5",
  "description": "A selectable pill that IS a checkbox (Multi Choice) or a radio in a group (Single Choice): a tap changes its choice, nothing else. The ChipsAction container (min s64 × s48 / s40); Title + optional Subtitle in one text container, an optional start slot, the control glyph on by default. States Standard · Active (the choice) · Skeleton; Disabled as its own property.",
  "files": {
    "spec": "specs/components/chips-choice/chips-choice.md",
    "a11y": "specs/components/chips-choice/chips-choice-a11y.md",
    "preview": "src/chips-choice.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon (components) — main file",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      ".M-ChipsChoice": {
        "key": "59c9d10966f5f8dbb5c5f649751de48f8b7fc385",
        "note": "Set 9991:11068 on page [ChipsChoice] 3.0 (10568:21887), private (dot-prefixed) — RTL × Chips Type (Multi Choice (Checkbox), Single Choice (Radio button)) × State (Default, Active, Skeleton) × Disabled (false, true); booleans 🔴Slot#1 (off), 🔵Slot#2 (the control, on), Subtitle; texts Title / Subtitle."
      },
      ".S-ChipsChoice": {
        "key": "f1e9a32259b2c494a617c444132415a76f6d2116",
        "note": "Set 10008:23290, same page, private — the same axes; min height s40."
      }
    },
    "capturedAt": "2026-10-29"
  },
  "root": {
    "class": "chips-choice",
    "dataDsComponent": "chips-choice"
  },
  "anatomy": {
    "content": {
      "class": "chips-choice__content",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "notes": "The row of the start slot, the text container and the control, gap s0; scales to 95% on press (pushButton)."
    },
    "startSlot": {
      "class": "chips-choice__slot--start",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "optional": true,
      "notes": "Figma 🔴Slot#1 (the set 10086:29717 shared with ChipsAction), off by default: hugs, min s24 — an icon (any DS icon, s24) or a Number Indicator L (its meaning a .visually-hidden phrase after the text). Figma also offers an Avatar — no web build. aria-hidden. Takes both classes — .chips-choice__slot (the box: centred, min s24, the svg size) and .chips-choice__slot--start (the name)."
    },
    "text": {
      "class": "chips-choice__text",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "notes": "Figma TextContainer: ONE centred column holding the Title and the Subtitle — s4 left / right padding, s0 top / bottom, s0 between the lines; its side padding is the gap to a slot, the control or the edge."
    },
    "title": {
      "class": "chips-choice__title",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "notes": "One line, Body/Main Body, the root colour. Capped at the container's width, it ends in an ellipsis (the accessible name stays whole)."
    },
    "subtitle": {
      "class": "chips-choice__subtitle",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "optional": true,
      "notes": "Off by default. One line, Caption/Caption, the root colour; a .visually-hidden \", \" separates it from the Title in the name."
    },
    "control": {
      "class": "chips-choice__control",
      "onlyWhen": {
        "state": [
          "standard",
          "active"
        ]
      },
      "optional": true,
      "notes": "Figma 🔵Slot#2 holding the private .↳🔵Controls glyph (10158:9884), on by default, s24. Both glyphs sit in the markup (.chips-choice__glyph--off / --on); aria-checked shows one. Multi: checkbox-unselected / checkbox-selected (the deprecated icon); Single: radio-unselected / radio-selected. TextAndIcon/Secondary when Standard, TextAndIcon/Primary when Active, TextAndIcon/Disabled when disabled. aria-hidden — a picture of the state. Off = the platforms' Filter preset."
    }
  },
  "axes": {
    "size": {
      "title": "Size",
      "type": "enum",
      "values": [
        "m",
        "s"
      ],
      "default": "m",
      "css": {
        "modifier": ".chips-choice--s",
        "mechanism": "m is the bare .chips-choice (min height s48); .chips-choice--s sets min height s40. Nothing else differs."
      },
      "figma": {
        "kind": "component-set",
        "values": {
          "m": ".M-ChipsChoice",
          "s": ".S-ChipsChoice"
        }
      },
      "notes": "The web presets — data-ds-preset=\"m\" / \"s\"."
    },
    "chipsType": {
      "title": "Chips Type",
      "type": "enum",
      "values": [
        "multi",
        "single"
      ],
      "default": "multi",
      "css": {
        "mechanism": "multi = role=\"checkbox\" with the checkbox glyphs; single = role=\"radio\" with the radio glyphs, inside a role=\"radiogroup\" (roving tabindex, arrow keys). No class of its own."
      },
      "figma": {
        "kind": "variant-property",
        "property": "Chips Type",
        "values": {
          "multi": "Multi Choice (Checkbox)",
          "single": "Single Choice (Radio button)"
        }
      }
    },
    "state": {
      "title": "State",
      "type": "enum",
      "values": [
        "standard",
        "active",
        "skeleton"
      ],
      "default": "standard",
      "css": {
        "mechanism": "standard = aria-checked=\"false\"; active = aria-checked=\"true\" (Surface/OnWhite fill, Border/Active ring, the check / dot glyph); .chips-choice--skeleton = an empty borderless s124 pill on Skeleton/OnWhite with --pattern-shimmer"
      },
      "figma": {
        "kind": "variant-property",
        "property": "State",
        "values": {
          "standard": "Default",
          "active": "Active",
          "skeleton": "Skeleton"
        }
      }
    },
    "disabled": {
      "title": "Disabled",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "the native disabled attribute: the Standard fill and ring whatever the choice, the content TextAndIcon/Disabled; an Active chip keeps its check or dot"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Disabled",
        "notes": "Its own variant property, not a State value — combines with Default and Active; no disabled Skeleton."
      }
    },
    "startSlot": {
      "title": "Start slot",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "the .chips-choice__slot--start element present or absent"
      },
      "figma": {
        "kind": "variant-property",
        "property": "🔴Slot#1",
        "notes": "A BOOLEAN component property (🔴Slot#1#9992:82), false by default."
      }
    },
    "subtitle": {
      "title": "Subtitle",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "the .chips-choice__subtitle element present or absent"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Subtitle",
        "notes": "A BOOLEAN (Subtitle#9987:45) plus the TEXT property Subtitle Text."
      }
    },
    "control": {
      "title": "Control",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "the .chips-choice__control element present or absent"
      },
      "figma": {
        "kind": "variant-property",
        "property": "🔵Slot#2",
        "notes": "A BOOLEAN component property (🔵Slot#2#10173:11), true by default. Off = the platforms' Filter preset (iOS DsChipsChoicePreset.filter, Android DsChip.ChoiceType.Filter) — not a Figma value."
      }
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "dir=\"rtl\" on the root or an ancestor: the row mirrors (the control left, the start slot right), the skeleton sweep reverses; the glyph is not mirrored (the Figma RTL variants reuse it)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "RTL"
      }
    },
    "fill": {
      "title": "Fill",
      "type": "token",
      "tokens": [
        "--background-primary"
      ],
      "default": "--background-primary",
      "customizable": "any DS colour token",
      "css": {
        "customProperty": "--chips-choice-fill"
      },
      "figma": {
        "kind": "none",
        "notes": "The Standard (and Disabled) fill; 3.1 changed it from Surface/OnGrey."
      }
    },
    "fillActive": {
      "title": "Fill (Active)",
      "type": "token",
      "tokens": [
        "--surface-on-white"
      ],
      "default": "--surface-on-white",
      "customizable": "any DS colour token",
      "css": {
        "customProperty": "--chips-choice-fill-active"
      },
      "figma": {
        "kind": "none"
      }
    },
    "borderColor": {
      "title": "Border colour",
      "type": "token",
      "tokens": [
        "--border-default"
      ],
      "default": "--border-default",
      "customizable": "any DS colour token",
      "css": {
        "customProperty": "--chips-choice-border"
      },
      "figma": {
        "kind": "none",
        "notes": "Drawn as an inset ring (box-shadow) that takes no space. Focus re-points the internal --chips-choice-ring to --border-inform."
      }
    },
    "borderColorActive": {
      "title": "Border colour (Active)",
      "type": "token",
      "tokens": [
        "--border-active"
      ],
      "default": "--border-active",
      "customizable": "any DS colour token",
      "css": {
        "customProperty": "--chips-choice-border-active"
      },
      "figma": {
        "kind": "none"
      }
    }
  },
  "states": {
    "default": [
      "standard",
      "active",
      "skeleton"
    ],
    "interaction": [
      "standard",
      "hover",
      "pressed",
      "focused",
      "disabled"
    ]
  },
  "constraints": [
    "The chip IS the control — role=\"checkbox\" (Multi) or role=\"radio\" in a role=\"radiogroup\" (Single); never a button with a separate checkbox inside, never a second target.",
    "aria-checked is the one source of the state: it shows the glyph and the Active fill / ring; a handler flips aria-checked and data-ds-state together and nothing else.",
    "Multi: Space toggles; Enter does nothing (a click synthesized by Enter is swallowed — the Checkbox trap). Single: one Tab stop per group — tabindex 0 on the checked option when it is enabled, else on the first enabled option, none when all are disabled, re-synced on every change; arrow keys move the focus and the choice together, wrap, skip disabled options, follow the visual direction in RTL and ignore modified keys; picking one clears every other option's choice, a disabled one too; Space checks, Enter does nothing.",
    "Hugs its content: min width s64, min height s48 (M) / s40 (S), padding s12 at the start and end and s0 top / bottom, radius s96 (full), a s2 inset ring that takes no space. The Skeleton is the one fixed size (s124).",
    "No Counter and no corner Indicator — those are ChipsAction's.",
    "Disabled keeps the Standard fill and ring whatever the choice; the content turns TextAndIcon/Disabled; never opacity.",
    "Press = pushButton (content 95% + --shadow-pressed overlay); the choice's fill eases on --transforming-state-default; the glyph swaps in the same frame. No motion tokens declared on the component and no reduced-motion override.",
    "With the control on, a chosen chip is told apart by its Border/Active ring and the glyph, never by the fill alone (≈ 1.1:1); on focus the glyph is the cue that survives. With the control off (the Filter preset) a focused Active chip differs by the fill alone and a disabled one not at all (the Standard fill and border); the state is still announced — flagged for the owner.",
    "Title and Subtitle are one line each; the chip is capped at its container's width and an over-long line ends in an ellipsis.",
    "Forced colours: an outline in system colours (ButtonBorder, thicker when checked; Highlight on focus; GrayText when disabled)."
  ],
  "analytics": {
    "dataDsComponent": "chips-choice",
    "actions": [
      "toggle",
      "select"
    ],
    "valueAttr": "data-ds-state",
    "notes": "Coverage unit, the root is the one target. data-ds-action = toggle (Multi) / select (Single); data-ds-preset = the size (m / s); data-ds-variant = the Chips Type (multi / single); data-ds-state = standard / active / skeleton — the choice, updated in the same handler as aria-checked. A disabled chip keeps its state and emits no tap."
  },
  "rtl": {
    "supported": true,
    "notes": "The row mirrors (the control on the left); the name keeps the language's reading order; the glyphs keep their orientation — the Figma RTL variants reuse the same glyph, so the check is not mirrored. In a Single Choice group the arrow keys follow the visual direction."
  }
}
```
