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

---

# RadioButton · Oymyakon DS 3

> A single-choice selection control: a s24 circle that carries a s12 dot when selected, wrapped in
> a s48 touch target.

**Version:** 3.1.1 · **Status:** Ready · **Figma:** `[RadioButton] 3.1` (node `17855:6503`)

---

## 1. Description

RadioButton lets a person pick exactly one option out of several. It is used in forms, settings,
payment-method and delivery-option lists, sorting and filter sheets — anywhere the choice is
mutually exclusive and all the options are worth showing at once.

A RadioButton is never meaningful on its own: it belongs to a group, and selecting one option
deselects the previously selected one. That exclusivity is the whole difference from
[Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md), where each control stands alone. A group also never returns to
an empty state through the control itself — once an option is picked, picking another one is the
only way forward, so the group carries a sensible initial selection or an explicit "None" option.

---

## 2. Anatomy

```
RadioButton                 .radiobutton        s48 x s48 touch target — required
└── Box                     .radiobutton__box   s24 x s24 circle — required
    └── Dot                 .radiobutton__dot   s12 dot — required, collapsed while unselected
```

| Element | Class | Required | Notes |
|---|---|---|---|
| Touch target | `.radiobutton` | required | s48 x s48, centres the box; the focusable element |
| Box | `.radiobutton__box` | required | s24 x s24, fully round (radius s12), s2 border |
| Dot | `.radiobutton__dot` | required | s12 circle, centred; collapsed to scale 0 while unselected |

The s48 target is larger than the s24 box on purpose: the visible box carries the design, the
target carries the accessible hit area (WCAG 2.5.5). In Figma these are two components —
`.RadioNoSafezone` (node `4368:7845`) is the bare s24 box for cases where the parent already
provides the hit area, and `[RadioButton] 3.1` wraps it in the s48 safe zone. The dot is the
`radio-icon` layer inside the box, not a separate component.

---

## 3. Variants and sizes

RadioButton has one size. Its variability is the state matrix in section 4 — the Figma component
set exposes exactly two variant axes:

| Axis | Values | Default |
|---|---|---|
| `State` | `Unselected` · `Selected` | `Unselected` |
| `Disabled` | `false` · `true` | `false` |

| Node | Purpose |
|---|---|
| `[RadioButton] 3.1` (`17855:6503`) | s48 touch target — the component consumers place |
| `.RadioNoSafezone` (`4368:7845`) | bare s24 box — for parents that own the hit area |
| `radio-icon` (`17855:4564`) | the s12 dot inside the box |

There is no error or read-only variant in the Figma contract. Validation is carried by the
surrounding form field, not by the control.

---

## 4. States

| State | Class | Box | Dot |
|---|---|---|---|
| **Standard** | `.radiobutton` | transparent, s2 border `TextAndIcon/Secondary` | collapsed |
| **Selected** | `.radiobutton--selected` | filled `TextAndIcon/Primary` | visible, `TextAndIcon/InversePrimary` |
| **Disabled** | `.radiobutton--disabled` | transparent, s2 border `TextAndIcon/Disabled` | collapsed |
| **Selected disabled** | `.radiobutton--selected.radiobutton--disabled` | filled `TextAndIcon/Disabled` | visible, `TextAndIcon/InversePrimary` |
| **Focus visible** | `:focus-visible` | s2 outline `TextAndIcon/Primary`, inset, radius s24 | unchanged |

The border colour matches the fill in both selected states, so the box keeps its s24 outer size
across the whole matrix — nothing shifts when the selection moves through a group.

Unselecting through the control is not a state of its own: a RadioButton leaves the selected state
only when another option in its group is selected.

---

## 5. Animation and behavior

Motion uses `Transforming/State/Default` from
[`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md) — the token
[`components-roadmap.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/components-roadmap.md) assigns to RadioButton, and the one
§6.6 catalogues for the neighbouring selection controls (Checkbox / Switch). §6.6 does not list
RadioButton itself yet.

| Event | Pattern | Token | CSS |
|---|---|---|---|
| Unselected → Selected | `Transforming/State/Default` | `--radiobutton-state-default` | `background-color`, `border-color`, `transform: scale(0 → 1)`, `opacity` |
| Selected → Unselected | `Transforming/State/Default` | `--radiobutton-state-default` | `background-color`, `border-color`, `transform: scale(1 → 0)`, `opacity` |
| Disabled change | `Transforming/State/Default` | `--radiobutton-state-default` | `border-color`, `background-color` |

```css
:root { --radiobutton-state-default: 200ms cubic-bezier(0.4, 0, 0.2, 1); }
@media (prefers-reduced-motion: reduce) {
  :root { --radiobutton-state-default: 0ms linear; }
}
```

The dot grows from the centre by `transform: scale()` and `opacity` — the Figma `Unselected`
variant holds the same `radio-icon` layer collapsed to zero radius at zero opacity, so the two
variants are one geometry with two dot sizes. Only compositor-friendly properties animate.
Reduced motion zeroes the token, so the state change becomes instant.

Within a group the outgoing option animates back to unselected while the incoming one animates in;
both run on the same token, so the swap reads as one movement.

---

## 6. Color tokens

One token name per role; Light/Dark substitution happens by name.

| Element | Token |
|---|---|
| Box border — Standard | `var(--text-and-icon-secondary)` |
| Box fill — Selected | `var(--text-and-icon-primary)` |
| Box border — Selected | `var(--text-and-icon-primary)` |
| Box border — Disabled | `var(--text-and-icon-disabled)` |
| Box fill — Selected disabled | `var(--text-and-icon-disabled)` |
| Dot | `var(--text-and-icon-inverse-primary)` |
| Focus outline | `var(--text-and-icon-primary)` |

The disabled state is expressed by a token swap, never by `opacity`.

---

## 7. Typography

RadioButton carries no text of its own. A label sits beside it as a separate element — in a list
row that is [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md), whose own typography applies. When a bare label is paired with
the control, it uses **Main Body** and is associated with the input (see
[`radiobutton-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/radiobutton.md)).

---

## 8. Spacing

| Property | Token | Value |
|---|---|---|
| Touch target | `var(--sp-s48)` | 48 x 48 |
| Box | `var(--sp-s24)` | 24 x 24 |
| Box radius | `var(--sp-s12)` | 12 — half of s24, so the box is a circle |
| Box border width | `var(--sp-s2)` | 2 |
| Dot | `var(--sp-s12)` | 12 x 12 |
| Dot radius | `var(--sp-s6)` | 6 — half of s12, so the dot is a circle |
| Target padding | `var(--sp-s0)` | 0 — the box is centred by flex |
| Focus outline width | `var(--sp-s2)` | 2, inset |
| Focus outline radius | `var(--sp-s24)` | 24 — half of s48, so the ring is round like the control |

The s12 gap between the target edge and the box is geometry, not padding: (48 − 24) / 2, produced
by centring. The s6 ring of fill visible around the dot in the selected state is geometry as well:
(24 − 12) / 2.

In a vertical group the targets stack with no gap between them — the s48 targets already provide
s12 of clear space above and below each s24 box.

---

## 9. Usage context

**When to use**

- Exactly one option out of several, with all the options visible at once (2–7 items).
- A choice that is confirmed later by the form, not applied the moment it is made.
- A list row that selects one item, via [Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md).

**When not to use**

- Any number of options, including none — that is [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md).
- An immediate on/off that applies without confirmation — that is Switch.
- More options than fit comfortably on screen — that is Select / Dropdown.
- Two or three short mutually exclusive options that read better as one control — that is
  [Segmented Control](https://super-dollop-pzmo65r.pages.github.io/segmented-control.md).
- A filter chip or removable token — that is [Tag](https://super-dollop-pzmo65r.pages.github.io/tag.md).

**Related components** — [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) (the multi-select counterpart),
[Cell](https://super-dollop-pzmo65r.pages.github.io/cell.md) (list row that hosts a RadioButton),
[Segmented Control](https://super-dollop-pzmo65r.pages.github.io/segmented-control.md), Switch, Select / Dropdown.

---

## 10. Accessibility

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

The s48 target satisfies WCAG 2.5.5 Target Size — the s24 box alone does not, so the target is
always the focusable and clickable element, never the box. The control exposes `role="radio"` with
`aria-checked` inside a `role="radiogroup"` container, and the group is one Tab stop: Tab moves to
the selected option, arrow keys move the selection between options. State is never communicated by
colour alone: the dot is the non-colour signal.

---

## 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` | `radiobutton` |
| Coverage unit | yes |
| Tap target model | root — the s48 target is the only target |
| Actions | `select` |
| Internal targets | none |
| Emits value | yes — `data-ds-state` reflects `selected` / `unselected` |

```html
<div role="radiogroup" aria-labelledby="payment-label">
  <span id="payment-label">Payment method</span>

  <button type="button" class="radiobutton radiobutton--selected"
          data-ds-component="radiobutton"
          data-ds-component-id="payment-cash"
          data-ds-state="selected"
          data-ds-action="select"
          role="radio" aria-checked="true" aria-labelledby="payment-cash-label"
          tabindex="0">
    <span class="radiobutton__box"><span class="radiobutton__dot"></span></span>
  </button>
  <span id="payment-cash-label">Cash</span>

  <button type="button" class="radiobutton"
          data-ds-component="radiobutton"
          data-ds-component-id="payment-card"
          data-ds-state="unselected"
          data-ds-action="select"
          role="radio" aria-checked="false" aria-labelledby="payment-card-label"
          tabindex="-1">
    <span class="radiobutton__box"><span class="radiobutton__dot"></span></span>
  </button>
  <span id="payment-card-label">Card</span>
</div>
```

The action is `select`, not `toggle`: a tap on an already selected option is a no-op, and the
control never returns to unselected by itself. Selecting an option emits `select` on the incoming
control; the outgoing one changes state without emitting.

When a visible label sits beside the control, `aria-labelledby` points at it — that is preferred
over `aria-label`, which would duplicate the text into a second, unsynchronised place. Use
`aria-label` only when there is no visible label.

`data-ds-state` is updated together with `aria-checked` on every selection change — on both the
incoming and the outgoing control — so the analytics value and the accessible value never diverge.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.1.1 | 2026-08-04 | The component consumes `var(--transforming-state-default)` directly — `--radiobutton-state-default` was a local alias holding the DS token's literal, and the motion pipeline made it redundant. The site page is rebuilt section by section: annotated canvases with pointers throughout, an equal gap between the shapes, Layout as a redline instead of a text read-out, Usage as a good/bad pair, and Accessibility rewritten from `radiobutton-a11y.md`. `capabilities.json` records `type="button"` on the root. |
| 3.1.0 | 2026-07-30 | Initial spec, aligned to Figma `[RadioButton] 3.1` (node `17855:6503`): State × Disabled (4 states), s48 target over a round s24 box, s12 dot scaling from the centre, `Transforming/State/Default` motion. |

---

# RadioButton — Accessibility

**Component:** RadioButton
**Version:** 3.1.0
**Spec:** [`radiobutton.md`](https://super-dollop-pzmo65r.pages.github.io/radiobutton.md)

---

## Role and ARIA

| Attribute | Value | Where |
|---|---|---|
| `role` | `"radiogroup"` | The container that wraps the options |
| `aria-labelledby` | id of the group's visible label | The `radiogroup` container |
| `role` | `"radio"` | Each s48 target (`.radiobutton`) |
| `aria-checked` | `"true"` / `"false"` | Each s48 target — mirrors the selected state |
| `aria-label` | The option's text | The s48 target, when no visible label is associated |
| `aria-labelledby` | id of the visible label | Preferred over `aria-label` when a label is on screen |
| `aria-disabled` | `"true"` | The s48 target, when disabled |

A native `<input type="radio">` carries the role, the checked state, and the group semantics on its
own. When the control is built from a `<button>` (the DS markup, so the box can be styled), `role`
and `aria-checked` are required — a `<button>` alone announces as a button and exposes no value.

The button also carries `type="button"`. Without it a `<button>` inside a `<form>` defaults to
`type="submit"`, so every activation would submit the form — including a TalkBack or VoiceOver
double tap, which activates the control through a synthesized `click` rather than a key press. That
would contradict the keyboard model below, where submitting is reserved for `Enter`.

The `radiogroup` wrapper is what makes the options a set: without it a screen reader announces
"1 of 1" for every option and the exclusivity is lost. The group carries its own accessible name,
so the question ("Payment method") is read before the first option.

`aria-checked` and `data-ds-state` are updated in the same handler — on both the incoming and the
outgoing option — so the accessible value and the analytics value never drift apart.

---

## Focus

The **s48 target** is the focusable element, never the s24 box — focusing the box would give a
24 x 24 hit area and fail WCAG 2.5.5 Target Size (minimum 24 x 24, with 44 x 44 recommended for
pointer input; the DS uses 48).

A radio group is **one Tab stop**, not one per option (roving `tabindex`):

| Situation | `tabindex` |
|---|---|
| The selected option | `0` |
| Every other option in the group | `-1` |
| No option selected yet | `0` on the first option, `-1` on the rest |

| Requirement | Implementation |
|---|---|
| Reachable by Tab | `<button>` is focusable natively; the roving `tabindex` above selects which one |
| Visible focus ring | `:focus-visible` — s2 inset outline in `var(--text-and-icon-primary)`, radius s24 |
| Disabled | Not focusable — `disabled` on the button, or `aria-disabled` + removed from tab order |

The focus ring is inset so it stays inside the s48 target and never collides with the neighbouring
option in a dense list; its s24 radius makes it round, matching the control.

Moving the selection with the arrow keys moves focus with it, so the focused option and the
selected option stay the same element.

---

## Keyboard

| Key | Action |
|---|---|
| `Tab` / `Shift+Tab` | Move focus into / out of the group — one stop for the whole group |
| `↓` / `→` | Move to the next option and select it; wraps from the last to the first |
| `↑` / `←` | Move to the previous option and select it; wraps from the first to the last |
| `Space` | Select the focused option; no effect when it is already selected |
| `Enter` | Nothing — reserved for submitting the surrounding form |

Arrow keys both move focus and change the selection — that is the platform convention for radio
groups, and it is what makes the group a single Tab stop. Disabled options are skipped by the arrow
keys rather than focused and refused.

`Enter` does not select — it stays free to submit the surrounding form.

**Implementation note.** Because the DS markup is a real `<button>` (chosen so the box can be
styled), the browser synthesizes a `click` when `Enter` is pressed on it. A bare `click` handler
would therefore select on `Enter`, contradicting the rule above. The handler flags `Enter` on
`keydown` and ignores the click it produces:

```js
let viaEnter = false;
radio.addEventListener('keydown', function (e) {
  if (e.key === 'Enter') { viaEnter = true; return; }
  if (e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); select(radio); }
  if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); select(next(radio)); }
  if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); select(prev(radio)); }
});
radio.addEventListener('click', function () {
  if (viaEnter) { viaEnter = false; return; }
  select(radio);
});
```

`select()` moves `aria-checked`, `data-ds-state`, the `--selected` class and `tabindex="0"` to the
incoming option, clears them on the outgoing one, and focuses the incoming target.

A native `<input type="radio">` needs none of this — grouping by `name` gives it arrow-key
navigation, the roving tab stop, and the `Enter` behaviour for free. The script is the cost of
styling the box.

---

## Contrast

| Pair | Requirement |
|---|---|
| Box border (Standard) vs background | ≥ 3:1 — non-text UI component contrast (WCAG 1.4.11) |
| Box fill (Selected) vs background | ≥ 3:1 |
| Dot vs box fill | ≥ 3:1 |
| Focus outline vs background | ≥ 3:1 |

`TextAndIcon/Disabled` is deliberately below the contrast floor: a disabled control is exempt from
1.4.11. Because of that, disabled state is never the only way information is conveyed.

State is never signalled by colour alone — the **dot** is the non-colour indicator that
distinguishes selected from unselected.

---

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Group | The question text | — | Radio group | — |
| RadioButton (unselected) | Option text | "Not checked, 2 of 4" | Radio button | "Double tap to select" |
| RadioButton (selected) | Option text | "Checked, 1 of 4" | Radio button | — |

The position in set ("2 of 4") comes from the group: TalkBack derives it from the `radiogroup`
container, which is why the wrapper is required rather than decorative.

### Edge states

- **Disabled:** announced as "Disabled" after the label; the option is skipped in swipe navigation and does not respond to double tap.
- **Selected disabled:** announces the label, "Checked", then "Disabled" — the value is still read, so the person knows what state is frozen.
- **Whole group disabled:** the group keeps its label and is announced once as disabled, instead of every option repeating it.
- **In a Cell row:** the row and the radio are one focus stop, announced as a single radio button with the row's text as the label — not two separate stops.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Group | The question text | — | Container | — |
| RadioButton (unselected) | Option text | — | Button, not selected | "Double tap to select" |
| RadioButton (selected) | Option text | — | Button, selected | — |

iOS has no native radio trait: the state is carried by the **selected** trait
(`accessibilityTraits.selected`) rather than by a value string, which is what VoiceOver users
expect on the platform. Grouping is expressed with an accessibility container so the question is
announced before the options.

### Edge states

- **Dimmed:** `accessibilityTraits.notEnabled` — VoiceOver appends "Dimmed"; the option stays in the rotor so its state is discoverable.
- **Selected dimmed:** announces the label, "Selected", then "Dimmed".
- **Whole group dimmed:** the container is announced once as dimmed; the options are not each annotated.
- **In a Cell row:** the row is one accessibility element; the radio's state is merged into the row's traits instead of being a separate element.

---

## Testing checklist

- [ ] Tab enters the group once and lands on the selected option (or the first one when nothing is selected)
- [ ] Arrow keys move the selection and wrap at both ends; disabled options are skipped
- [ ] Space selects the focused option; `Enter` does not
- [ ] Selecting one option clears `aria-checked` on the previously selected one — exactly one is `true`
- [ ] The focus ring is visible against both Light and Dark backgrounds
- [ ] The full s48 target is clickable, not only the s24 box
- [ ] TalkBack announces the group label, then label + "Checked" / "Not checked" + position in set
- [ ] VoiceOver announces label + the selected trait
- [ ] Disabled options are not reachable by Tab and do not respond to Space
- [ ] Selected state is distinguishable with colour filters on (the dot carries it)

---

## Machine contract — `specs/components/radiobutton/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": "radiobutton",
  "name": "RadioButton",
  "version": "3.1.1",
  "description": "Single-choice selection control: a round s24 box carrying a s12 dot when selected, wrapped in a s48 touch target. One size; variability is the State x Disabled matrix.",
  "files": {
    "spec": "specs/components/radiobutton/radiobutton.md",
    "a11y": "specs/components/radiobutton/radiobutton-a11y.md",
    "preview": "src/radiobutton.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.28.0 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "[RadioButton] 3.1": {
        "key": "25db3861211ee4ae14689a2670ecc665e04736bf",
        "note": "The s48 touch target — what consumers place. Node 17855:6503. It wraps the private .RadioNoSafezone set (node 4368:7845), which is not published to the library and therefore has no stable key."
      }
    },
    "capturedAt": "2026-07-30"
  },
  "root": {
    "class": "radiobutton",
    "dataDsComponent": "radiobutton"
  },
  "anatomy": {
    "box": {
      "class": "radiobutton__box",
      "notes": "Figma .RadioNoSafezone (private, node 4368:7845). s24 x s24, radius s12 so it is a circle, s2 border; centred inside the s48 target by flex, so the s12 inset is geometry rather than padding."
    },
    "dot": {
      "class": "radiobutton__dot",
      "notes": "Figma radio-icon (node 17855:4564). s12 circle in --text-and-icon-inverse-primary, centred in the box, leaving a s6 ring of fill around it. Collapsed to scale 0 at zero opacity while unselected — the Unselected Figma variant holds the same layer at zero radius."
    }
  },
  "axes": {
    "selected": {
      "title": "Selected",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".radiobutton--selected"
      },
      "figma": {
        "kind": "variant-property",
        "property": "State",
        "values": {
          "false": "Unselected",
          "true": "Selected"
        }
      },
      "notes": "Selected fills the box with --text-and-icon-primary and grows the dot from the centre. The border colour matches the fill so the box keeps its s24 outer size across the matrix."
    },
    "disabled": {
      "title": "Disabled",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".radiobutton--disabled"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Disabled",
        "values": {
          "false": "false",
          "true": "true"
        }
      },
      "notes": "Expressed by a colour-token swap to --text-and-icon-disabled, never by opacity. Combines with selected for the fourth state."
    }
  },
  "states": {
    "default": [
      "standard",
      "selected",
      "disabled",
      "selected-disabled",
      "focus-visible"
    ]
  },
  "constraints": [
    "The s48 target is the focusable and clickable element, never the s24 box — focusing the box alone would give a 24 x 24 hit area and fail WCAG 2.5.5 Target Size.",
    "A RadioButton is only meaningful inside a group: the assembling markup wraps the options in role=\"radiogroup\" with its own accessible name, otherwise the exclusivity and the position in set are lost.",
    "Selection is exclusive — selecting one option clears the previously selected one. The control never returns to unselected by itself, so the action is select, not toggle.",
    "The group is one Tab stop (roving tabindex: 0 on the selected option, -1 on the rest); arrow keys move the selection and skip disabled options.",
    "There is no error or read-only variant: [RadioButton] 3.1 exposes only State x Disabled. Validation belongs to the surrounding form field — do not invent a state on the control.",
    "RadioButton carries no text of its own. A label is a separate element beside it; in a list row that element is Cell.",
    "The root carries type=\"button\". Without it a <button> inside a <form> defaults to type=\"submit\", so every activation submits — including a TalkBack or VoiceOver double tap, which fires a synthesized click rather than a key press.",
    "Disabled is a token swap, never an opacity multiplier (component-authoring rule).",
    "State is never signalled by colour alone — the dot is the non-colour indicator.",
    "aria-checked and data-ds-state are written in the same handler, on both the incoming and the outgoing option, so the accessible value and the analytics value cannot drift apart.",
    "Space selects; Enter does not, so Enter stays free to submit the surrounding form.",
    "One size only. The s24 box, s12 dot and s48 target are fixed — a smaller radio is not a supported customization.",
    "Motion is Transforming/State/Default in both directions — var(--transforming-state-default), declared DS-wide in tokens/generated/motion.css and zeroed there under prefers-reduced-motion, so the component carries no override of its own."
  ],
  "analytics": {
    "dataDsComponent": "radiobutton",
    "action": "select",
    "valueAttr": "data-ds-state",
    "notes": "Coverage unit; the root s48 target is the only tap target and there are no internal targets. data-ds-state carries selected | unselected, and is rewritten on the outgoing option as well as the incoming one. Only the incoming control emits the select action."
  },
  "rtl": {
    "supported": true,
    "notes": "The control is symmetric — the box carries no directional content, so no mirroring is needed. Its position relative to a label follows the parent's writing direction."
  }
}
```
