Selection
The controls that answer "which of these" — one of several, on or off, and the chips that show what a reader already picked.
Spec of record: design-system/components/forms/SegmentGroup.d.ts
Live
Real widgets, built from the real kit. Turn a knob and it rebuilds; the theme bar above applies the design system's own tokens — including anything you changed under Customize — and the device selector uses the same sizes the golden baselines gate on, so what you see here is what CI holds the widget to.
Press Focus on any preview to take it to the full window — at a desktop width that renders most devices at 1:1, which is when the widget's own controls become clickable. The bar always states the device and the scale, so a shrunk preview says it is shrunk. Esc leaves.
Every specimen at once, in one state. Per-specimen controls would rebuild the wall a board exists to remove.
Dart for these values
family_selection() What it is made of
The widgets this screen assembles, with their full API below. They used to live on a second entry of their own; one thing you choose gets one page.
AppSegmentGroup widget
The design-system SegmentGroup — the closed-enum editor at 2–5 options: ONE row of equal-width segments sharing one hairline outline, filling the field width like every box editor. 6+ options is a Select; an open enum is a Combobox; a boolean stays a Checkbox row. Selection is the applied-toggle…
Full dartdoc
The design-system SegmentGroup — the closed-enum editor at 2–5 options: ONE row of equal-width segments sharing one hairline outline, filling the field width like every box editor. 6+ options is a Select; an open enum is a Combobox; a boolean stays a Checkbox row. Selection is the applied-toggle grammar: accent text (body weight-em) + accent border + DesignTokens.accentTint fill on the selected segment. Deliberately NO check glyph — the ink/border/tint shift is the one encoding, and a ✓ beside it would encode twice. **Fit rule** (deterministic, no config): labels render on one line and are never ellipsized. When any label cannot fit its equal share at the ambient text scale — measured at the selected weight, so selecting cannot change fit — or the share drops under the spec's 64px minimum, the control morphs to the **wrap posture**: the same segments as standalone hairline-outlined toggle tiles wrapping onto further rows (same paint, same semantics, same order). **Keyboard** (Radio semantics — this IS the exclusive choice, re-anatomized): arrow keys move the selection to the next/previous non-disabled option AND select it, wrapping past the ends; focus follows the new selection (roving). Enter/Space select the focused segment. Segments never un-check — tapping the selected one is a no-op. ```dart AppSegmentGroup<String>( label: 'Priority', value: priority, onChanged: setPriority, options: const [ (value: 'routine', label: 'Routine', disabled: false), (value: 'elevated', label: 'Elevated', disabled: false), (value: 'critical', label: 'Critical', disabled: false), ], ) ```
AppSegmentGroupdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| disabled | bool | false | Disables every segment regardless of onChanged — the state contract's disabled paint plus the field-family lock mark on the micro-label. |
| error | String? | null | Group-level inline error. When non-null it renders as the danger error line below the row — the same anatomy and typography as `AppSelect`'s error line. Default null — no error anatomy. |
| isRequired | bool | false | Renders the required marker — the trailing accent `*` on the micro-label (the field-family marker). Visual only; no-op when label is null. |
| label | String? | null | Optional micro-label rendered above the row (DesignTokens.label). |
| onChanged | ValueChanged<T>? | null | Receives the tapped option's value. Null (or disabled) disables every segment in the group. |
| options * | List<AppSegmentOption<T>> | — | The exclusive options this group offers, in display order. 2–5 — more is a Select. Each entry's `label` renders inside its segment; `disabled` mutes just that segment even when the group is enabled. |
| value | T? | null | The group's current selection, or `null` if none is selected yet. |
AppRadio widget
The design-system Radio — a themed, token-driven pressable claiming the M3 radio archetype. A single exclusive-choice option: value is this option's identity, groupValue is the group's current selection, and this radio renders selected when they're equal.
Full dartdoc
The design-system Radio — a themed, token-driven pressable claiming the M3 radio archetype. A single exclusive-choice option: value is this option's identity, groupValue is the group's current selection, and this radio renders selected when they're equal. Circular indicator, 16px, centered in a hit area at least DesignTokens.touch (44 logical px) square. Two forms, chosen by whether label is supplied: **bare** (just the circle — pass title for the tooltip and accessible name) and **label** (circle + DesignTokens.body text in a row — the common form; the whole row is one tap target and one Semantics node). Paint: selected renders an DesignTokens.accent ring with an DesignTokens.accent center dot — **no filled background, no row tint** (Radio.d.ts: the circle is anatomy, stays round in every theme; a filled radio reads as a checkbox). Unselected renders a DesignTokens.chkBorder ring on DesignTokens.paper, no dot. State contract (all honored): enabled · hover (DesignTokens.ink4 wash across the full hit area, pointer enhancement only) · focus-visible (1px solid accent, zero offset) · pressed (no separate pressed paint beyond the state flip — the circle repaints on release) · disabled (onChanged null → DesignTokens.ink40 ring/dot, not tappable). No ripple. Radios never un-check themselves — tapping an already-selected radio is a no-op; selection only ever moves to a different value via a sibling radio (or programmatically via AppRadioGroup). ```dart AppRadio<String>( value: 'routine', groupValue: priority, label: 'Routine', onChanged: setPriority, ) ```
AppRadiodefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| focusNode | FocusNode? | null | Optional external focus node — AppRadioGroup supplies one per option so arrow-key selection can rove focus onto the newly selected radio. Null keeps the internal node (current behavior). |
| groupValue * | T? | — | The group's current selection, or `null` if none is selected yet. |
| label | String? | null | Optional row label, styled DesignTokens.body. When present the circle and label render as one row and one tap target. When absent, only the bare circle renders — pass title for its accessible name. |
| onChanged | ValueChanged<T>? | null | Receives value when this radio is activated. Radios never un-check themselves — not invoked when already selected. Null disables the radio (not tappable, muted paint). |
| title | String? | null | Tooltip and accessibility label. Required in practice for the bare form (the circle alone has no visible text); ignored as the accessible name when label is set (the label speaks for the row), but still feeds the Tooltip. |
| value * | T | — | This option's identity. |
AppRadioGroup widget
The design-system RadioGroup — a vertical, single-select group of AppRadios claiming the M3 radio-group archetype. For **2–5 always-visible exclusive options** (Radio.prompt.md); more options, or options arriving from data, is a Select instead.
Full dartdoc
The design-system RadioGroup — a vertical, single-select group of AppRadios claiming the M3 radio-group archetype. For **2–5 always-visible exclusive options** (Radio.prompt.md); more options, or options arriving from data, is a Select instead. Renders one AppRadio per entry in options, each at least DesignTokens.touch tall, selecting value and firing onChanged with the tapped option's value. disabled mutes the whole group (each radio renders disabled, `onChanged` un-wired) without the caller threading it through every option. **Keyboard**: with focus anywhere inside the group, arrow keys move the selection to the next/previous non-disabled option AND select it (Radio.d.ts: "Arrow keys move AND select; roving tabindex") — focus follows the new selection. Down/Right advance, Up/Left go back, both wrapping past the ends. Enter/Space select the focused radio. ```dart AppRadioGroup<String>( label: 'Priority', value: priority, onChanged: setPriority, options: const [ (value: 'routine', label: 'Routine'), (value: 'elevated', label: 'Elevated'), (value: 'critical', label: 'Critical — dispatch review'), ], ) ```
AppRadioGroupdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| axis | Axis | Axis.vertical | Layout axis for the options. Axis.vertical (default) stacks the options in a column — current behavior. Axis.horizontal lays them in a wrapping row (the spec's row posture), options separated by the group's row gap. |
| disabled | bool | false | Disables every option in the group regardless of onChanged. |
| error | String? | null | Group-level inline error (Radio.d.ts `error`). When non-null it renders as the danger error line below the options — the same anatomy and typography as `AppSelect`'s error line. Default null — no error anatomy (current behavior). |
| isRequired | bool | false | Renders the required marker — the trailing accent `*` on the micro-label (design-system pin `5908e48`). Visual only; no-op when label is null. |
| label | String? | null | Optional micro-label rendered above the group (DesignTokens.label). |
| onChanged | ValueChanged<T>? | null | Receives the tapped option's value. Null (or disabled) disables every option in the group. |
| options * | List<AppRadioOption<T>> | — | The exclusive options this group offers, in display order. Each entry's `label` renders next to its radio; `disabled` mutes just that option even when the group itself is enabled. |
| value | T? | null | The group's current selection, or `null` if none is selected yet. |
AppCheckbox widget
The design-system Checkbox — a themed, token-driven tri-state pressable claiming the M3 checkbox archetype. A 16px box centered in a hit area at least DesignTokens.touch (44 logical px) square.
Full dartdoc
The design-system Checkbox — a themed, token-driven tri-state pressable claiming the M3 checkbox archetype. A 16px box centered in a hit area at least DesignTokens.touch (44 logical px) square. Two forms, chosen by whether label is supplied: **bare** (just the box — table cells; pass title for the tooltip and accessible name) and **label** (box + DesignTokens.body text in a row — settings/filter rows; the whole row is one tap target and one Semantics node). Tri-state: value is `true` (checked), `false` (unchecked), or `null` (indeterminate — the select-all "some but not all" state). Per M3 select-all behavior, tapping while indeterminate resolves to checked (`onChanged(true)`), never to unchecked. Paint: checked fills DesignTokens.accent with a check glyph in DesignTokens.onAccent; indeterminate fills DesignTokens.accent with a dash glyph in DesignTokens.onAccent; unchecked is DesignTokens.paper with a DesignTokens.chkBorder hairline border (the 3:1 boundary). Radius DesignTokens.rXs, the chip/checkbox radius. State contract (all honored): enabled · hover (DesignTokens.ink4 wash across the full hit area, pointer enhancement only) · focus-visible (1px solid accent, zero offset) · pressed (no separate pressed paint beyond the state flip — the box repaints on release) · disabled (onChanged null → DesignTokens.ink40 foreground/border, not tappable). No ripple. ```dart AppCheckbox(value: row.selected, title: 'Select row', onChanged: (v) => select(row.id, v)) AppCheckbox( value: allSelected ? true : (someSelected ? null : false), title: 'Select all', onChanged: toggleAll, ) AppCheckbox(value: prefs.archived, label: 'Include archived', onChanged: setArchived) ```
AppCheckboxdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| label | String? | null | Optional row label, styled DesignTokens.body. When present the box and label render as one row and one tap target (settings/filter rows). When absent, only the bare box renders (table cells) — pass title for its accessible name. |
| onChanged | ValueChanged<bool>? | null | Receives the next boolean state. Indeterminate resolves to `true`. Null disables the checkbox (not tappable, muted paint). |
| title | String? | null | Tooltip and accessibility label. Required in practice for the bare form (the box alone has no visible text); ignored as the accessible name when label is set (the label speaks for the row), but still feeds the Tooltip. |
| value * | bool? | — | Tri-state value — `true` checked, `false` unchecked, `null` indeterminate. |
AppSwitch widget
The design-system Switch — a themed, token-driven pressable claiming the M3 switch archetype. **Instant-effect toggles only** (settings, view options) — the value takes effect the moment it's tapped. Deferred form state (submitted later via a save action) stays `AppCheckbox`.
Full dartdoc
The design-system Switch — a themed, token-driven pressable claiming the M3 switch archetype. **Instant-effect toggles only** (settings, view options) — the value takes effect the moment it's tapped. Deferred form state (submitted later via a save action) stays `AppCheckbox`. A 34x20 track with a 12px thumb, centered in a hit area at least DesignTokens.touch (44 logical px) square. Two forms, chosen by whether label is supplied: **bare** (just the track — pass title for the tooltip and accessible name) and **label** (track + DesignTokens.body text in a row — settings rows; the whole row is one tap target and one Semantics node). Paint: off renders a DesignTokens.paper track with a DesignTokens.hairS hairline border and a DesignTokens.ink60 thumb; on renders a DesignTokens.accent track (border matches) with a DesignTokens.onAccent thumb. No icons inside the thumb. Track and thumb share DesignTokens.rS — square in house themes; a rounder theme pills both shapes with zero widget edits. State contract (all honored): enabled · hover (DesignTokens.ink4 wash across the full hit area, pointer enhancement only) · focus-visible (1px solid accent, zero offset) · pressed (no separate pressed paint beyond the state flip — the track repaints on release) · disabled (onChanged null → DesignTokens.ink40 track/border, DesignTokens.paper thumb when off or DesignTokens.ink40 track with a DesignTokens.paper thumb when on; not tappable). No ripple. ```dart AppSwitch(value: sync, label: 'Auto-sync on cellular', onChanged: setSync) AppSwitch(value: dense, title: 'Compact rows', onChanged: setDense) ```
AppSwitchdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| label | String? | null | Optional row label, styled DesignTokens.body. When present the track and label render as one row and one tap target (settings rows). When absent, only the bare track renders — pass title for its accessible name. |
| onChanged | ValueChanged<bool>? | null | Receives the next boolean state. Null disables the switch (not tappable, muted paint). |
| title | String? | null | Tooltip and accessibility label. Required in practice for the bare form (the track alone has no visible text); ignored as the accessible name when label is set (the label speaks for the row), but still feeds the Tooltip. |
| value * | bool | — | Whether the switch is on. |
AppChip widget
The design-system Chip — a themed, token-driven pressable claiming the M3 input-chip archetype. **Removable selections only** — applied filters, picked values. A chip with no onRemove is almost always the wrong widget; per the constitution, a status or neutral enum value is a word in the row, never…
Full dartdoc
The design-system Chip — a themed, token-driven pressable claiming the M3 input-chip archetype. **Removable selections only** — applied filters, picked values. A chip with no onRemove is almost always the wrong widget; per the constitution, a status or neutral enum value is a word in the row, never a chip (encode once). When onRemove is null the "x" glyph does not render at all — the chip degrades to a plain read-only label, never a disabled-but-visible remove control. THE CONTAINED CHIP (`Chip.d.ts`, 2026-09-29, #893 — the owner's answer to #869): every chip is set in the UI FONT, never the data font — the data role is for IDs, counts and numerals, and a chip is words. It is 28 tall at DesignTokens.rXs, 12.5/500, padded 10 at the inline start and 3 before the "x" (10 when there is none). A chip reads as KEY and VALUE when it has a name: the name first at the regular weight and 75% opacity, then label at the chip's weight, 6 apart — no middle dot is drawn between them (the weight and the opacity are the separator). The chip's accessible name says both halves. accent marks the applied-filter state: the DesignTokens.accentTint fill with DesignTokens.accent text and NO outline. The default (non-accent) form is DesignTokens.paper with the DesignTokens.cardEdge outline and DesignTokens.ink60 text. State contract (all honored): enabled · hover (the "x" strengthens to DesignTokens.ink, pointer enhancement only) · focus-visible on the "x" (1px solid accent, zero offset — the "x" is a real focusable/tappable target, independent of the chip body) · pressed (the "x"'s hit area, no separate chip-body press) · accent (selected styling, see above). No disabled state: a chip that cannot be removed simply omits onRemove rather than rendering a muted, non-functional "x". ```dart AppChip(label: 'Crew B-7', onRemove: () => drop('crew')) AppChip( name: 'Status', label: 'Blocked, In progress', accent: true, onRemove: () => drop('status'), ) ```
AppChipdefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| accent | bool | false | Applied-filter styling: DesignTokens.accent text over the DesignTokens.accentTint fill, no outline. Defaults to the plain form. |
| label * | String | — | The chip's text in the UI font — the VALUE half of a key/value chip ("Blocked, In progress"), or the whole chip ("Crew B-7", a picked value). One line, never wrapped — a long label ellipsizes. |
| mark | AppChipMark? | null | THE VALUE'S MARK (`Chip.d.ts` `mark`, 2026-10-01, #977): a facet chip for a value of a column with FORMAT RULES carries the mark that value wears in the cell — the glyph in its color's ink, or with no glyph the 8px square in its fill inside the pin rim — drawn FIRST, 6 before the name, so a colored… |
| name | String? | null | The KEY half of a key/value chip ("Status"), drawn before label at the regular weight and 75% opacity (`Chip.d.ts` THE CONTAINED CHIP). Null draws the label alone. |
| onRemove | VoidCallback? | null | Fires when the "x" is tapped. Null hides the "x" entirely (a read-only chip) — never rendered muted/disabled. |
| title | String? | null | Tooltip and accessibility hint for the chip's label region. The "x" carries its own independent `Remove <name> <label>` accessible name regardless of title. |
Types its knobs take
These are not things you choose — they are the shapes the knobs above expect. They live under this entry rather than in the navigation for exactly that reason.
AppChipMark support
A chip's VALUE MARK (`Chip.d.ts` `mark`: `{color?, glyph?}`) — the color and glyph a formatted value wears, without the rule's paint or text treatments (a chip keeps its own).
| Field | Type | Default | What it does |
|---|---|---|---|
| color | KitColor? | null | The mark's color — the square's fill, or the glyph's ink. |
| glyph | KitIconRef? | null | The mark's glyph, or null for the color square. |
Where it lives
| Class | File |
|---|---|
| AppSegmentGroup | packages/app_widgets/lib/src/kit/forms/segment_group.dart |
| AppRadio | packages/app_widgets/lib/src/kit/forms/radio.dart |
| AppRadioGroup | packages/app_widgets/lib/src/kit/forms/radio.dart |
| AppCheckbox | packages/app_widgets/lib/src/kit/forms/checkbox.dart |
| AppSwitch | packages/app_widgets/lib/src/kit/forms/switch.dart |
| AppChip | packages/app_widgets/lib/src/kit/forms/chip.dart |