@antadesign/anta 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/NOTICES.md +32 -1
  2. package/dist/anta_helpers.d.ts +25 -0
  3. package/dist/anta_helpers.js +28 -0
  4. package/dist/box-observation.d.ts +8 -0
  5. package/dist/box-observation.js +41 -0
  6. package/dist/box-types.d.ts +2 -0
  7. package/dist/bundle.css +1 -1
  8. package/dist/bundle.js +77 -46
  9. package/dist/components/Banner.js +2 -2
  10. package/dist/components/Box.d.ts +28 -12
  11. package/dist/components/Box.js +18 -11
  12. package/dist/components/Breadcrumbs.css +1 -1
  13. package/dist/components/Button.js +2 -2
  14. package/dist/components/Calendar.js +3 -5
  15. package/dist/components/Card.js +2 -2
  16. package/dist/components/Checkbox.d.ts +12 -13
  17. package/dist/components/Checkbox.js +5 -13
  18. package/dist/components/Expander.js +2 -2
  19. package/dist/components/Input.d.ts +8 -2
  20. package/dist/components/Input.js +6 -2
  21. package/dist/components/InputAutocomplete.js +3 -12
  22. package/dist/components/InputDate.css +1 -1
  23. package/dist/components/InputDate.js +2 -0
  24. package/dist/components/InputTime.js +2 -2
  25. package/dist/components/Loader.js +2 -1
  26. package/dist/components/Menu.d.ts +4 -4
  27. package/dist/components/Menu.js +1 -1
  28. package/dist/components/MenuItem.d.ts +7 -8
  29. package/dist/components/MenuItem.js +4 -4
  30. package/dist/components/Panel.d.ts +17 -0
  31. package/dist/components/Panel.js +22 -0
  32. package/dist/components/Progress.js +2 -2
  33. package/dist/components/RadioGroup.d.ts +14 -14
  34. package/dist/components/RadioGroup.js +7 -23
  35. package/dist/components/Select.d.ts +26 -21
  36. package/dist/components/Select.js +51 -38
  37. package/dist/components/SelectFaceted.d.ts +24 -11
  38. package/dist/components/SelectFaceted.js +65 -44
  39. package/dist/components/Slider.js +3 -3
  40. package/dist/components/Steps.css +1 -1
  41. package/dist/components/Switch.d.ts +10 -11
  42. package/dist/components/Switch.js +3 -11
  43. package/dist/components/TabPanel.d.ts +3 -8
  44. package/dist/components/TabPanel.js +2 -1
  45. package/dist/components/Tabs.d.ts +2 -2
  46. package/dist/components/Tabs.js +4 -3
  47. package/dist/components/Tag.d.ts +7 -9
  48. package/dist/components/Tag.js +2 -2
  49. package/dist/components/Text.js +2 -2
  50. package/dist/components/Title.js +2 -2
  51. package/dist/components/Tooltip.d.ts +1 -1
  52. package/dist/components/Tooltip.js +1 -1
  53. package/dist/components/select-parts.css +1 -1
  54. package/dist/elements/a-avatar.css +1 -1
  55. package/dist/elements/a-banner.css +1 -1
  56. package/dist/elements/a-box.css +1 -1
  57. package/dist/elements/a-box.d.ts +6 -6
  58. package/dist/elements/a-box.js +74 -23
  59. package/dist/elements/a-button.css +1 -1
  60. package/dist/elements/a-button.d.ts +16 -10
  61. package/dist/elements/a-button.js +64 -1
  62. package/dist/elements/a-calendar.css +1 -1
  63. package/dist/elements/a-capture.css +1 -1
  64. package/dist/elements/a-card.css +1 -1
  65. package/dist/elements/a-checkbox.css +1 -1
  66. package/dist/elements/a-checkbox.d.ts +3 -0
  67. package/dist/elements/a-checkbox.js +17 -1
  68. package/dist/elements/a-copy.css +1 -1
  69. package/dist/elements/a-dialog.css +1 -1
  70. package/dist/elements/a-expander.css +1 -1
  71. package/dist/elements/a-expander.js +1 -0
  72. package/dist/elements/a-icon.css +1 -1
  73. package/dist/elements/a-icon.shapes.css +1 -1
  74. package/dist/elements/a-input-time.css +1 -1
  75. package/dist/elements/a-input-time.d.ts +8 -0
  76. package/dist/elements/a-input-time.js +120 -15
  77. package/dist/elements/a-input.css +1 -1
  78. package/dist/elements/a-input.d.ts +27 -1
  79. package/dist/elements/a-input.js +180 -41
  80. package/dist/elements/a-loader.css +1 -1
  81. package/dist/elements/a-menu-group.css +1 -1
  82. package/dist/elements/a-menu-item.css +1 -1
  83. package/dist/elements/a-menu-item.js +1 -0
  84. package/dist/elements/a-menu-separator.css +1 -1
  85. package/dist/elements/a-menu.css +1 -1
  86. package/dist/elements/a-menu.d.ts +8 -13
  87. package/dist/elements/a-menu.js +146 -45
  88. package/dist/elements/a-panel.css +1 -0
  89. package/dist/elements/a-panel.d.ts +16 -0
  90. package/dist/elements/a-panel.js +153 -0
  91. package/dist/elements/a-progress.css +1 -1
  92. package/dist/elements/a-radio-group.css +1 -1
  93. package/dist/elements/a-radio.css +1 -1
  94. package/dist/elements/a-radio.d.ts +4 -0
  95. package/dist/elements/a-radio.js +18 -0
  96. package/dist/elements/a-slider.css +1 -1
  97. package/dist/elements/a-slider.d.ts +1 -0
  98. package/dist/elements/a-slider.js +3 -6
  99. package/dist/elements/a-switch.css +1 -1
  100. package/dist/elements/a-switch.d.ts +3 -0
  101. package/dist/elements/a-switch.js +17 -1
  102. package/dist/elements/a-tab.css +1 -1
  103. package/dist/elements/a-tabpanel.css +1 -1
  104. package/dist/elements/a-tabs.css +1 -1
  105. package/dist/elements/a-tag.css +1 -1
  106. package/dist/elements/a-text.css +1 -1
  107. package/dist/elements/a-text.js +1 -0
  108. package/dist/elements/a-title.css +1 -1
  109. package/dist/elements/a-toast.css +1 -1
  110. package/dist/elements/a-toaster.css +1 -1
  111. package/dist/elements/a-tooltip.css +1 -1
  112. package/dist/elements/a-tooltip.d.ts +3 -4
  113. package/dist/elements/a-tooltip.js +30 -8
  114. package/dist/elements/focus.d.ts +9 -0
  115. package/dist/elements/focus.js +41 -0
  116. package/dist/elements/index.d.ts +1 -0
  117. package/dist/elements/index.js +3 -0
  118. package/dist/elements/panel-focus.d.ts +10 -0
  119. package/dist/elements/panel-focus.js +86 -0
  120. package/dist/elements/shadow-aria.d.ts +10 -0
  121. package/dist/elements/shadow-aria.js +155 -0
  122. package/dist/general_types.d.ts +70 -42
  123. package/dist/generate-icons.mjs +3 -0
  124. package/dist/index.d.ts +5 -2
  125. package/dist/index.js +2 -0
  126. package/dist/jsx-runtime.d.ts +10 -1
  127. package/dist/reset.css +1 -1
  128. package/dist/theme-antithesis.css +1 -0
  129. package/dist/theme-antune.css +1 -0
  130. package/dist/tokens.css +1 -1
  131. package/docs/accessibility.md +4 -2
  132. package/docs/changelog.md +135 -0
  133. package/docs/colors.md +7 -7
  134. package/docs/comparison.md +2 -2
  135. package/docs/components/box.md +98 -34
  136. package/docs/components/breadcrumbs.md +75 -6
  137. package/docs/components/button.md +2 -2
  138. package/docs/components/capture.md +8 -8
  139. package/docs/components/checkbox.md +21 -16
  140. package/docs/components/input-autocomplete.md +6 -0
  141. package/docs/components/input-date.md +8 -2
  142. package/docs/components/input.md +99 -40
  143. package/docs/components/menu.md +5 -6
  144. package/docs/components/panel.md +189 -0
  145. package/docs/components/radio.md +16 -13
  146. package/docs/components/select-faceted.md +58 -2
  147. package/docs/components/select.md +72 -59
  148. package/docs/components/steps.md +4 -4
  149. package/docs/components/switch.md +16 -10
  150. package/docs/components/tabs.md +21 -8
  151. package/docs/components/tag.md +5 -3
  152. package/docs/components/tooltip.md +6 -4
  153. package/docs/index.md +1 -0
  154. package/docs/install-config.md +23 -12
  155. package/docs/normalization.md +6 -4
  156. package/docs/theming.md +95 -17
  157. package/package.json +8 -4
  158. package/dist/components/InputDate.module.css +0 -1
  159. package/dist/components/Select.module.css +0 -1
  160. package/dist/components/SelectFaceted.module.css +0 -1
  161. package/dist/search-index.json +0 -0
  162. package/dist/theme-anta.css +0 -1
@@ -14,6 +14,12 @@ application-defined facet keys, not Anta terms. Each facet has one of four
14
14
  Use `value` with `onValueChange` to control it, or use `defaultValue` for an
15
15
  uncontrolled filter.
16
16
 
17
+ Without `searchable`, the trigger opens a menu of facets. With `searchable`, it
18
+ opens a dialog containing the search field and that menu. A filtered option
19
+ facet also opens a dialog containing its search field and options menu. Text and
20
+ custom facets use dialogs for their editable content. An unfiltered option facet
21
+ keeps the menu popup pattern.
22
+
17
23
  Pass `placement` to control where the root facet menu opens relative to its
18
24
  trigger. It uses the same values as `Menu` and still flips or clamps when space
19
25
  is limited. `offset` sets the gap in pixels between the trigger and the menu:
@@ -22,11 +28,32 @@ is limited. `offset` sets the gap in pixels between the trigger and the menu:
22
28
  <SelectFaceted placement="top-end" offset={8} facets={FACETS} />
23
29
  ```
24
30
 
31
+ Open **Metadata** in the first demo to choose key–value tags such as
32
+ **env production** or **region us-east**. Its `renderOption` callback reads
33
+ custom option fields and renders a `Tag` with `label` and `value`. Search
34
+ for **production** to see the same tag in global results.
35
+
25
36
  ```tsx
26
37
  const FACETS = [
27
38
  { key: 'assignee', label: 'Assignee', kind: 'multiple', filter: true, options: people },
28
39
  { key: 'owner', label: 'Owner', kind: 'single', filter: true, options: people },
29
40
  { key: 'status', label: 'Status', kind: 'single', options: [{ value: 'open', label: 'Open' }, /* … */] },
41
+ {
42
+ key: 'metadata',
43
+ label: 'Metadata',
44
+ kind: 'multiple',
45
+ icon: 'tag',
46
+ filter: true,
47
+ options: [
48
+ { value: 'env:production', label: 'env: production', dataKey: 'env', dataValue: 'production' },
49
+ { value: 'env:staging', label: 'env: staging', dataKey: 'env', dataValue: 'staging' },
50
+ { value: 'region:us-east', label: 'region: us-east', dataKey: 'region', dataValue: 'us-east' },
51
+ { value: 'team:platform', label: 'team: platform', dataKey: 'team', dataValue: 'platform' },
52
+ ],
53
+ renderOption: (option) => (
54
+ <Tag label={String(option.dataKey)} value={String(option.dataValue)} />
55
+ ),
56
+ },
30
57
  { key: 'title', label: 'Title contains', kind: 'text' },
31
58
  { key: 'duration', label: 'Min duration', kind: 'custom', summary: (v) => `≥ ${v.min}s`, render: /* … */ },
32
59
  ]
@@ -68,6 +95,7 @@ function Demo() {
68
95
  selection={facet.kind}
69
96
  indicator={facet.kind === 'single' ? 'check' : undefined}
70
97
  options={facet.options}
98
+ renderOption={facet.renderOption}
71
99
  value={value[facet.key]}
72
100
  onValueChange={(v) => setFacet(facet.key, v)}
73
101
  leading={`${facet.label}:`}
@@ -120,6 +148,27 @@ The trigger opens the facet menu. The example renders one editable
120
148
  [Select](./select.md) below it for each active facet. Each Select receives the
121
149
  facet label through `leading`, which displays it as a prefix.
122
150
 
151
+ ### Custom option content
152
+
153
+ Pass `renderOption(option, state)` on a `single` or `multiple` facet to replace
154
+ each option's label, hint, and icon layout. It runs in both the facet flyout and
155
+ global search results. `state` contains `value`, `selected`, and `disabled`.
156
+ Selection handling, indicators, and option styling remain on the row.
157
+ Return `null` to use the default layout for an option.
158
+
159
+ The first demo's **Metadata** facet stores the key and value in custom option
160
+ fields. Pass them to Tag's `label` and `value` props:
161
+
162
+ ```tsx
163
+ renderOption: (option) => (
164
+ <Tag label={String(option.dataKey)} value={String(option.dataValue)} />
165
+ )
166
+ ```
167
+
168
+ Filtering still uses `value`, `label`, and `hint`, or the facet's custom `filter`
169
+ function. Keep searchable text in those fields. Single-facet summaries still
170
+ use the option label, and multiple-facet summaries show the selected count.
171
+
123
172
  ### Value and changes
124
173
 
125
174
  The value is a record keyed by facet key: `{ [facetKey]: value }`. Its shape
@@ -373,11 +422,12 @@ const recency: SelectFacetCustom<Recency> = {
373
422
  | `onValueChange?` | (value, attrs) => void | — | Fires after any facet changes. `value` is the whole new record (a facet key → that facet's value; a cleared facet is absent). `attrs` says what changed: `{ facet, kind, value }` for a single facet edit, or `{ all: true }` for the "Clear all" row — narrow on `'all' in attrs` before reading `facet`. |
374
423
  | `placement?` | 'left' \| 'right' \| 'bottom' \| 'top' \| 'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end' \| 'right-start' \| 'right-end' \| 'left-start' \| 'left-end' | bottom-start | Preferred placement of the root filter menu relative to its trigger. The menu auto-flips vertically and clamps horizontally when needed. |
375
424
  | `priority?` | 'primary' \| 'secondary' | secondary | Default trigger's button priority. |
376
- | `renderTrigger?` | (state) => ReactNode | — | Replaces the default `Button` with a trigger returned from this function. Receives a `SelectFacetedTriggerState`. Return exactly one focusable element: the menu is positioned relative to that element and opens when it is clicked. Add `aria-haspopup="menu"` and `aria-expanded={state.open}` to the returned element, on a role that supports them (an Anta `Button` already carries `role="button"`; otherwise add `role="combobox"`). `className`, `style`, and other trigger props apply only to the default Button, so add styling and attributes to the returned element. |
425
+ | `renderTrigger?` | (state) => ReactNode | — | Replaces the default `Button` with a trigger returned from this function. Receives a `SelectFacetedTriggerState`. Return exactly one focusable element: the menu is positioned relative to that element and opens when it is clicked. Add `aria-haspopup={searchable ? 'dialog' : 'menu'}` and `aria-expanded={state.open}` to the returned element, on a role that supports them (an Anta `Button` already carries `role="button"`; otherwise add `role="combobox"`). `className`, `style`, and other trigger props apply only to the default Button, so add styling and attributes to the returned element. |
377
426
  | `searchable?` | boolean | — | Adds a search field at the top of the root menu. It searches the options of every `single` and `multiple` facet in one list. For example, "alice" can appear under application-defined Assignee and Owner facets. `text` and `custom` facets remain available when the search is empty. Each facet uses its `filter` function when supplied, or the built-in substring match. |
378
427
  | `searchPlaceholder?` | string | Filter… | Placeholder for the global search field. |
379
428
  | `size?` | 'small' \| 'medium' \| 'large' | medium | Default trigger's button size. |
380
- | `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to a selected option row in the facet flyouts (label, selected tint, and the check / checkbox indicator). A named tone or a custom CSS color, matching `Select`'s `toneSelected`. Defaults to a neutral selection. |
429
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Default option-row tone in facet flyouts. An option's own `tone` wins. |
430
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply the default row tone in every state, or only to selected rows. An option's own `toneScope` wins. |
381
431
  | `value?` | SelectFacetedValue | — | Controlled value record, keyed by facet. When provided, update it through `onValueChange`. Leave it undefined for uncontrolled use. |
382
432
 
383
433
  ### The `SelectFacet` type
@@ -390,6 +440,12 @@ properties as [Select](./select.md). They use `kind` in place of Select's
390
440
  `selection`. `text` and `custom` do not use Select options. A `single` facet
391
441
  returns one option value; a `multiple` facet returns an array of option values.
392
442
 
443
+ When `selectAll` is enabled, Alt-clicking an option in a `multiple` facet clears
444
+ the other values and selects that option. Use Option-click on macOS. After a
445
+ 700ms hover, a tooltip beside the row describes the shortcut. Moving to another
446
+ row restarts the delay. An option's `tooltip` replaces that hint; set it
447
+ to `''` to hide the hint.
448
+
393
449
  | Field | Type | Description |
394
450
  |---|---|---|
395
451
  | `key` | `string` | The key for this facet's value in the record. `attrs.facet` returns it. It must be unique in `facets`. |
@@ -4,7 +4,7 @@
4
4
  field shows the selected value. Click the field to open the menu. The menu
5
5
  closes when a user clicks outside it or presses Esc.
6
6
 
7
- Select uses an [Input](./input.md) for its field and a [Menu](./menu.md) for its
7
+ Select uses an [Anta Button](./button.md) for its field and a [Menu](./menu.md) for its
8
8
  dropdown. Pass `options` as an array of strings or objects. Use `value` with
9
9
  **`onValueChange`** to control the selection, or use `defaultValue` for an
10
10
  uncontrolled Select.
@@ -40,8 +40,8 @@ skipped by the keyboard.
40
40
  <Select options={OPTIONS} value={one} onValueChange={setOne} /> // tint only (default)
41
41
 
42
42
  // Tone the selected row: named tone or custom color (the indicator adopts it)
43
- <Select indicator="check" toneSelected="brand" options={OPTIONS} value={one} onValueChange={setOne} />
44
- <Select indicator="radio" toneSelected="#c026d3" options={OPTIONS} value={one} onValueChange={setOne} />
43
+ <Select indicator="check" tone="brand" toneScope="selected" options={OPTIONS} value={one} onValueChange={setOne} />
44
+ <Select indicator="radio" tone="#c026d3" toneScope="selected" options={OPTIONS} value={one} onValueChange={setOne} />
45
45
 
46
46
  // Multiple: checkboxes, count summary, Select-all row (default); value is an array
47
47
  <Select
@@ -70,7 +70,8 @@ its box goes mixed when only some are on. Pass **`selectAll={false}`** to drop i
70
70
 
71
71
  In a multi-select, **`Alt`+Click** a row to clear every other selection and
72
72
  select that row. On macOS, use Option instead of Alt.
73
- The standard tooltip describes this shortcut when the pointer is over a row.
73
+ After hovering for 700ms, the standard tooltip describes this shortcut beside
74
+ the row. Moving to another row restarts the delay.
74
75
  Set `SelectOption.tooltip` to replace it, or to `''` to hide it. The shortcut
75
76
  is available only when `selectAll` is enabled, so `selectAll={false}` removes it.
76
77
 
@@ -78,9 +79,9 @@ Each checkable row is the control itself (`role="menuitemcheckbox"` /
78
79
  `menuitemradio`, `aria-checked`); the checkbox / radio / check is a passive indicator
79
80
  the row drives.
80
81
 
81
- **`toneSelected`** tones the selected row(s): the whole row takes the tone, including
82
- label, icon, indicator, and tint. Pass a named tone or any CSS color (a custom color
83
- keeps its hue, with lightness pinned to the brand text). It reads strongest with
82
+ Use **`tone`** with **`toneScope="selected"`** to tone only selected rows: the whole
83
+ row takes the tone, including label, icon, indicator, and tint. Pass a named tone or
84
+ any CSS color (a custom color keeps its hue, with lightness pinned to the brand text). It reads strongest with
84
85
  `indicator` `none` / `check`, which show the row tint; `radio` / `checkbox` have no
85
86
  row tint, so it tones the label and indicator only.
86
87
 
@@ -105,12 +106,14 @@ option's **value, label, or hint**. It's **whitespace-flexible** (a typed space
105
106
  matches any run of whitespace) and **bolds the matched substring** in the results.
106
107
  Pass a **function** `(option, query) => boolean` for custom matching (no highlight).
107
108
 
108
- With `filter`, Select behaves as a **combobox**. Opening it focuses the search
109
- field. ↑ and ↓ move through matching options while focus
110
- stays in the field. Enter selects the active option, and Esc
111
- closes the menu. The search field stays visible while the results scroll. A
112
- "No matches" row appears when nothing matches, and the query resets when the
113
- menu closes. In `multiple` mode, `selectAll` affects only the visible matches.
109
+ With `filter`, the trigger opens a dialog containing the search field and an
110
+ options menu. This keeps the textbox outside the menu in the accessibility tree.
111
+ Opening the dialog focuses the search field. ↑ and ↓ move
112
+ through matching options while focus stays in that field. Enter
113
+ selects the active option, and Esc closes the dialog. The search field
114
+ stays visible while the results scroll. A "No matches" row appears when nothing
115
+ matches, and the query resets when the dialog closes. In `multiple` mode,
116
+ `selectAll` affects only the visible matches.
114
117
 
115
118
  ## Value and changes
116
119
 
@@ -229,7 +232,7 @@ section marks the leaf everywhere it occurs (selection is value-keyed).
229
232
  <Select status="info" statusIcon="sparkles" hint="Custom glyph" options={OPTIONS} defaultValue="stream" />
230
233
  ```
231
234
 
232
- The trigger is an [Input](./input.md), so it inherits the field props.
235
+ The trigger is an [Anta Button](./button.md) styled as a field.
233
236
  **`size`** (`small` · `medium` · `large`) sets the height and type scale.
234
237
  **`status`** paints a validation tone on the border, `hint`, and chevron, and
235
238
  prefixes a glyph (`info` / `success` / `warning` / `critical`); **`statusIcon`**
@@ -245,7 +248,7 @@ the glyph and, for `critical`, validity. See the
245
248
  ## Customization
246
249
 
247
250
  Select has no `<a-select>` element. The React and Preact wrapper coordinates an
248
- Input and a Menu. You can use the wrapper, render custom option content with
251
+ Anta Button and a Menu. You can use the wrapper, render custom option content with
249
252
  `renderOption`, replace its selection mark with `renderIndicator`, or build the
250
253
  same interaction from the raw elements.
251
254
 
@@ -259,22 +262,21 @@ uses the same values as `Menu` and still flips or clamps when space is limited.
259
262
 
260
263
  ### Build it from elements
261
264
 
262
- The wrapper renders a read-only `<Input>` and a `<Menu>`. It updates the field
265
+ The wrapper renders a `<Button>` immediately followed by a `<Menu>`, with a label
266
+ and hint inside an `<a-select-field>` layout wrapper. It updates the field
263
267
  and each option's `selected` state from the value, and reports a pick through
264
268
  `onValueChange`. Menu handles positioning, dismissal, and keyboard navigation.
265
269
 
266
270
  The following example shows the equivalent markup without React or Preact. Place
267
- the Menu directly after the Input so it can use that Input as its trigger.
271
+ the Menu directly after the Button so it can use that Button as its trigger.
268
272
 
269
273
  ```html
270
274
  <!-- `.select` scopes this example's CSS. Use an application selector instead. -->
271
275
  <div class="select" style="width: 240px">
272
- <!-- Read-only field trigger; `dim-actions` dims the chevron at rest. The
273
- trailing slot supplies the inline inset (--input-trailing-inset); the
274
- transition on the chevron matches <Select>. -->
275
- <a-input readonly dim-actions value="stream" aria-haspopup="menu">
276
- <a-icon slot="trailing" shape="chevron-down" style="transition: transform 150ms ease"></a-icon>
277
- </a-input>
276
+ <a-button role="button" tabindex="0" aria-label="Field" aria-haspopup="menu" aria-expanded="false">
277
+ <a-button-label>stream</a-button-label>
278
+ <a-icon shape="chevron-down" aria-hidden="true" style="transition: transform 150ms ease"></a-icon>
279
+ </a-button>
278
280
  <a-menu role="menu">
279
281
  <a-menu-item role="menuitemradio" tabindex="0" aria-checked="false" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
280
282
  <a-menu-item role="menuitemradio" tabindex="0" aria-checked="true" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
@@ -284,12 +286,14 @@ the Menu directly after the Input so it can use that Input as its trigger.
284
286
  <script type="module">
285
287
  import '@antadesign/anta/elements'
286
288
  const root = document.querySelector('.select')
287
- const field = root.querySelector('a-input')
289
+ const field = root.querySelector('a-button')
288
290
  const menu = root.querySelector('a-menu')
289
291
  const chevron = field.querySelector('a-icon')
290
292
  // Flip the chevron with the menu's open state (as <Select> does).
291
293
  menu.addEventListener('statechange', (e) => {
292
- chevron.style.transform = e.detail.next === 'open' ? 'rotate(180deg)' : ''
294
+ const open = e.detail.next === 'open'
295
+ field.setAttribute('aria-expanded', String(open))
296
+ chevron.style.transform = open ? 'rotate(180deg)' : ''
293
297
  })
294
298
  // On pick: mark the row selected + reflect its label into the field.
295
299
  menu.addEventListener('click', (e) => {
@@ -300,12 +304,16 @@ the Menu directly after the Input so it can use that Input as its trigger.
300
304
  el.toggleAttribute('selected', selected)
301
305
  el.setAttribute('aria-checked', String(selected))
302
306
  })
303
- field.value = item.querySelector('a-menu-item-label').textContent
307
+ field.querySelector('a-button-label').textContent = item.querySelector('a-menu-item-label').textContent
304
308
  })
305
309
  </script>
306
310
  ```
307
311
 
308
312
  The React and Preact `Select` wrapper performs those updates for you.
313
+ Because the Menu directly follows the Anta Button, it associates the focused
314
+ field with the popup through a direct ARIA element reference. No popup ID is
315
+ required. If a custom composition supplies its own `aria-controls`, Anta keeps
316
+ that authored relationship.
309
317
 
310
318
  ### Custom option rendering
311
319
 
@@ -461,7 +469,7 @@ empty selection still shows the `placeholder` — and takes the resolved `select
461
469
  options (`selected.length` is the count). Return `undefined` for any case you'd rather
462
470
  leave to the default, as above for the single-label case.
463
471
 
464
- Return a **string**: it flows into the same read-only field the default uses, so a
472
+ Return a **string**: it flows into the same Button label the default uses, so a
465
473
  long summary **ellipsizes at the field's width** the way a long value does
466
474
  (`Engineering, Design, … `). For rich content — chips, several nodes — reach for
467
475
  `renderTrigger` below, which replaces the whole field.
@@ -523,7 +531,8 @@ focusable element, such as the Anta `Button` in this example. Select positions
523
531
  the menu relative to that element and opens it when it is clicked. A fragment,
524
532
  multiple sibling elements, or a non-focusable wrapper prevents Select from finding
525
533
  the trigger. It logs a console warning when that happens. Add
526
- `aria-haspopup="menu"` and `aria-expanded={state.open}` to the element.
534
+ `aria-haspopup={filter ? 'dialog' : 'menu'}` and
535
+ `aria-expanded={state.open}` to the element.
527
536
 
528
537
  `state` includes `open`, `value`, `selected`, `disabled`, and `icon`. `selected`
529
538
  is the resolved option list, so `selected.length` is the multi-select count. The
@@ -633,11 +642,11 @@ scannable list instead of hiding matches behind flyouts.
633
642
  | `defaultValue?` | V \| V[] | — | Initial selected option value for uncontrolled use. |
634
643
  | `disabled?` | boolean | — | Disable the whole select. |
635
644
  | `filter?` | boolean \| (option, query) => boolean | — | Add a search field at the top of the menu that filters the options as you type. `true` uses the built-in matcher — a case-insensitive substring of the option's **value / label / hint**. Pass a **function** `(option, query) => boolean` for custom matching (called per option; return `true` to keep it). |
636
- | `hint?` | string | — | Helper text under the field (Input's `hint`). |
637
- | `icon?` | IconShape | — | Leading icon shown at the left of the field (the default trigger's `Input` `leading` slot). With a custom `renderTrigger`, it's passed through as `state.icon` instead — the consumer places it. |
645
+ | `hint?` | string | — | Helper text under the field. |
646
+ | `icon?` | IconShape | — | Leading icon shown at the left of the field. With a custom `renderTrigger`, it's passed through as `state.icon` instead — the consumer places it. |
638
647
  | `indicator?` | 'none' \| 'check' \| 'radio' | none | The per-row mark for **single**-select: `'none'` (a tint-only highlight), `'check'` (a trailing checkmark on the selected row, keeping the tint — the canonical Select look), or `'radio'` (a leading radio on every row). Multi-select always uses checkboxes. |
639
- | `label?` | string | — | Field label, above the trigger (Input's `label`). |
640
- | `leading?` | ReactNode | — | Content for the default trigger's `leading` slot, such as a key prefix before the value. It replaces the icon derived from `icon`. Include an `<Icon>` in this content when both are needed. Ignored by `renderTrigger`. |
648
+ | `label?` | string | — | Field label, above the trigger. |
649
+ | `leading?` | ReactNode | — | Content before the default trigger's value, such as a key prefix before the value. It replaces the icon derived from `icon`. Include an `<Icon>` in this content when both are needed. Ignored by `renderTrigger`. |
641
650
  | `offset?` | number | 4 | Gap in pixels between the trigger and the options menu. |
642
651
  | `onValueChange?` | (value, attrs) => void | — | Fires after the selection changes, with the new value and a `{ value, option }` snapshot. Select has no discrete element state, so there is no cancelable `onStateChange` (see the Input event-model note). |
643
652
  | `placeholder?` | string | — | Text shown when nothing is selected. |
@@ -645,18 +654,19 @@ scannable list instead of hiding matches behind flyouts.
645
654
  | `renderEmpty?` | (state) => ReactNode | — | Render content in the menu body when the (filtered) option list is empty — a "no results" message, a loading indicator (gated on your own external loading state), or a "create from the query" row. Receives an `EmptyState` (`query`, trimmed). There is no built-in empty message: when omitted, an empty list renders nothing. Whatever you return goes where the option rows would — a plain node is inert; return a `MenuItem` (e.g. a "Create" row) to make it focusable and selectable. |
646
655
  | `renderIndicator?` | (state) => ReactNode | — | Replace each row's selection **mark** with your own node, drawn at the leading edge. The row stays the control (`role` + `aria-checked` from `indicator` / `selection`); only the drawn mark changes, so pair it with an `indicator` (`'check'` / `'radio'`) or `selection="multiple"` for the semantics. Composes with `renderOption`. |
647
656
  | `renderOption?` | (option, state) => ReactNode | — | Replaces the built-in `label`, `hint`, and `icon` layout for each option row. Select still supplies the row container, click handling, ARIA attributes, and selection indicator. Read extra option fields through `SelectOption`'s index signature. `state` contains `value`, `selected`, and `disabled`. Filtering still matches the option's `value`, `label`, and `hint`, but Select cannot highlight matches within the returned content. |
648
- | `renderSummary?` | (selected) => string \| undefined | — | `multiple` only: build the trigger's selection summary text yourself, replacing the built-in "`All` / one label / `N selected`" logic. Receives the resolved selected options (`selected.length` is the count) and runs only while something is selected — an empty selection still shows the `placeholder`. Return a **string**: it flows into the default trigger's read-only field, so a long summary ellipsizes at the field's width just like a long value (`Engineering, Design, … `). Return `undefined` to fall back to the default for that case (e.g. customize only the count, keeping the single-label case built-in). For rich content (chips, multiple nodes) use `renderTrigger`, which replaces the whole field. |
649
- | `renderTrigger?` | (state) => ReactNode | — | Replaces the default field with a trigger returned from this function. Receives `open`, `value`, `selected`, `disabled`, and `icon`. Return exactly one focusable element, such as an Anta `Button`. The menu is positioned relative to that element and opens when it is clicked. Do not return a fragment, multiple siblings, or a non-focusable wrapper. Add `aria-haspopup="menu"` and `aria-expanded={state.open}` to the element, on a role that supports them (an Anta `Button` already carries `role="button"`; otherwise add `role="combobox"`). Field props (`label`, `hint`, `size`, `status`, `placeholder`, and `round`) and `className` / `style` apply only to the default field. Add styling and attributes to the returned element instead. |
657
+ | `renderSummary?` | (selected) => string \| undefined | — | `multiple` only: build the trigger's selection summary text yourself, replacing the built-in "`All` / one label / `N selected`" logic. Receives the resolved selected options (`selected.length` is the count) and runs only while something is selected — an empty selection still shows the `placeholder`. Return a **string**: it flows into the default trigger's Button label, so a long summary ellipsizes at the field's width just like a long value (`Engineering, Design, … `). Return `undefined` to fall back to the default for that case (e.g. customize only the count, keeping the single-label case built-in). For rich content (chips, multiple nodes) use `renderTrigger`, which replaces the whole field. |
658
+ | `renderTrigger?` | (state) => ReactNode | — | Replaces the default field with a trigger returned from this function. Receives `open`, `value`, `selected`, `disabled`, and `icon`. Return exactly one focusable element, such as an Anta `Button`. The menu is positioned relative to that element and opens when it is clicked. Do not return a fragment, multiple siblings, or a non-focusable wrapper. Add `aria-haspopup={filter ? 'dialog' : 'menu'}` and `aria-expanded={state.open}` to the returned button. An Anta `Button` already carries the correct role. Field props (`label`, `hint`, `size`, `status`, `placeholder`, and `round`) and `className` / `style` apply only to the default field. Add styling and attributes to the returned element instead. |
650
659
  | `round?` | boolean \| number \| string | — | Round the field corners — `true` for fully round, or a number / CSS length. |
651
660
  | `selectAll?` | boolean | true | `multiple` only: shows a "Select all" row that toggles every enabled option, or only the visible options when a filter query is active. Its checkbox is mixed when some options are selected. It is on by default. Set it to `false` to remove the row and the Alt/Option-click shortcut that selects only one row. |
652
661
  | `selectAllLabel?` | string | Select all | Label for the `selectAll` row. |
653
662
  | `selection?` | 'single' \| 'multiple' | single | Selection mode. `'single'` (the default) keeps `value` a single value and closes the menu on pick. Switch to `'multiple'` for checkboxes + an array value. |
654
663
  | `size?` | 'small' \| 'medium' \| 'large' | medium | Field size. |
655
- | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | neutral | Validation/feedback tone for the field (Input's `status`). |
656
- | `statusIcon?` | (string & {}) \| false \| IconShape | — | Glyph shown before the `hint` when `status` is set (Input's `statusIcon`). Each status has a default; pass a shape to override, or `false` to drop it. |
657
- | `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to the **selected** row(s) — the whole row takes this tone (label, icon, indicator, and the background tint), like passing `tone` to just the chosen option. A named tone or a custom CSS color. Most visible with the tint-based marks (`indicator` `'none'` / `'check'`); with `'radio'` / `'checkbox'` it tones the label + indicator (those modes have no row tint). |
664
+ | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | neutral | Validation/feedback tone for the field. |
665
+ | `statusIcon?` | (string & {}) \| false \| IconShape | — | Glyph shown before the `hint` when `status` is set. Each status has a default; pass a shape to override, or `false` to drop it. |
666
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Default option-row tone. An option's own `tone` wins. A named tone or a custom CSS color. Most visible with tint-based marks (`indicator` `'none'` / `'check'`); with `'radio'` / `'checkbox'` it also tones the indicator. |
667
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply the default row tone in every state, or only to selected rows. An option's own `toneScope` wins. |
658
668
  | `value?` | V \| V[] | — | Controlled value: the selected option's `value`. Update it through `onValueChange`. Leave it undefined for uncontrolled use. |
659
- | `verbose?` | boolean | — | `multiple` only: spell the picks out in the count summary — `3 selected: A, B, C` (labels comma-joined) in place of the bare `3 selected`. Applies to the multi-count case only: `All` stays `All`, a single pick stays its own label, and an empty selection stays the `placeholder`. The list flows into the read-only field, so it ellipsizes at the field's width when long (`3 selected: Engineering, Des… `). `renderSummary` overrides this. |
669
+ | `verbose?` | boolean | — | `multiple` only: spell the picks out in the count summary — `3 selected: A, B, C` (labels comma-joined) in place of the bare `3 selected`. Applies to the multi-count case only: `All` stays `All`, a single pick stays its own label, and an empty selection stays the `placeholder`. The list flows into the Button label, so it ellipsizes at the field's width when long (`3 selected: Engineering, Des… `). `renderSummary` overrides this. |
660
670
 
661
671
  ### The `SelectItem` type
662
672
 
@@ -756,16 +766,16 @@ inline `SelectGroup`, and a flyout `SelectSubmenu` (whose items are themselves
756
766
 
757
767
  Use the web component directly when you are not using React or Preact and a native control does not fit.
758
768
 
759
- `Select` has no host element. Compose a read-only `<a-input>` trigger with an
769
+ `Select` uses a layout wrapper. Compose an `<a-button>` trigger with an
760
770
  `<a-menu>` of options. The menu owns opening and dismissal; the short controller
761
771
  reflects a picked option into the field and selected row.
762
772
 
763
773
  ```html
764
774
  <div data-anta-composition="select" style="display: grid; gap: 4px; width: 280px">
765
- <a-input readonly value="stream">
766
- <span slot="label">Output</span>
767
- <span slot="trailing"><a-icon shape="chevron-down" aria-hidden="true"></a-icon></span>
768
- </a-input>
775
+ <a-button role="button" tabindex="0" aria-label="Output" aria-haspopup="menu" aria-expanded="false">
776
+ <a-button-label>stream</a-button-label>
777
+ <a-icon shape="chevron-down" aria-hidden="true"></a-icon>
778
+ </a-button>
769
779
  <a-menu role="menu">
770
780
  <a-menu-item role="menuitem" tabindex="0" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
771
781
  <a-menu-item role="menuitem" tabindex="0" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
@@ -775,8 +785,11 @@ reflects a picked option into the field and selected row.
775
785
  import '@antadesign/anta/elements'
776
786
 
777
787
  const root = document.querySelector('[data-anta-composition="select"]')
778
- const field = root.querySelector('a-input')
788
+ const field = root.querySelector('a-button')
779
789
  const menu = root.querySelector('a-menu')
790
+ menu.addEventListener('statechange', (event) => {
791
+ field.setAttribute('aria-expanded', String(event.detail.next === 'open'))
792
+ })
780
793
 
781
794
  menu.addEventListener('click', (event) => {
782
795
  const item = event.target instanceof Element ? event.target.closest('a-menu-item') : null
@@ -785,7 +798,7 @@ reflects a picked option into the field and selected row.
785
798
  const selected = option === item
786
799
  option.toggleAttribute('selected', selected)
787
800
  })
788
- field.value = item.querySelector('a-menu-item-label').textContent.trim()
801
+ field.querySelector('a-button-label').textContent = item.querySelector('a-menu-item-label').textContent.trim()
789
802
  })
790
803
  </script>
791
804
  ```
@@ -823,36 +836,36 @@ corners.
823
836
 
824
837
  ## Styling
825
838
 
826
- Select is an [Input](./input.md) plus a [Menu](./menu.md), so it
827
- inherits both surfaces' hooks. Reach for props first, then plain CSS or `::part` for
839
+ Select composes an [Anta Button](./button.md) and a [Menu](./menu.md). Reach for props first, then plain CSS or `::part` for
828
840
  the rest. Don't override an element's internal `--*` output tokens.
829
841
 
830
- **Selection color** routes through the props: a per-option `tone`, or `toneSelected`
831
- for the chosen row(s). Both take a named tone or any CSS color (a custom color keeps
842
+ **Selection color** routes through `tone`: set it per option or on the Select as a
843
+ default, then use `toneScope="selected"` for chosen rows only. It takes a named tone or any CSS color (a custom color keeps
832
844
  its hue, with lightness pinned to the brand text).
833
845
 
834
- **The field** is styled through Input's props (`size`, `status`, `round`) and its
835
- `::part`s; set its width with `style` / `className` (forwarded to the field). **The
836
- popover** takes Menu's `::part(menu)`.
846
+ **The field** takes `size`, `status`, and `round`. Set its width with `style` /
847
+ `className`, forwarded to the `<a-select-field>` wrapper. Style the trigger through
848
+ `a-select-field > a-button` and its `a-button-label`. The trigger uses
849
+ `:focus-visible` for its focus ring and ellipsizes long values. **The popover**
850
+ takes Menu's `::part(menu)`.
837
851
 
838
852
  ```tsx
839
853
  // Tone the selection; give the trigger a width.
840
- <Select toneSelected="brand" style={{ width: '220px' }} options={OPTIONS} />
854
+ <Select tone="brand" toneScope="selected" style={{ width: '220px' }} options={OPTIONS} />
841
855
  ```
842
856
 
843
857
  **Borderless trigger.** The field's border is an inset `box-shadow`, so you can drop
844
858
  it *at rest* and let the standard hover / focus shadow return on its own; no need to
845
859
  re-declare it. Pair that with hiding the chevron for a field that reads as plain text
846
- until you reach for it. Hide the chevron by targeting the trailing `a-icon` (its slot
847
- wrapper is inline-`display:contents`, so hit the icon itself).
860
+ until you reach for it. Hide the chevron by targeting `a-select-chevron`.
848
861
 
849
862
  ```css
850
863
  /* Drop the resting border-shadow; the standard hover / focus shadow still shows. */
851
- .ghost-select a-input:not(:hover):not(:focus-within)::part(field) {
864
+ .ghost-select a-select-field > a-button:not(:hover):not(:focus-visible) {
852
865
  box-shadow: none;
853
866
  }
854
867
  /* Center the chosen label in the field. */
855
- .ghost-select a-input::part(input) { text-align: center; }
856
- /* The chevron's slot wrapper is inline display:contents; hide the icon itself. */
857
- .ghost-select a-input [slot="trailing"] a-icon { display: none; }
868
+ .ghost-select a-select-field > a-button > a-button-label { text-align: center; }
869
+ /* Hide the chevron. */
870
+ .ghost-select a-select-chevron { display: none; }
858
871
  ```
@@ -260,10 +260,10 @@ Events match [`Tabs`](./tabs.md#events).
260
260
  | `onStateChange?` | (event, detail) => void | — | Fired whenever the active tab changes — event-first. `detail` is `{ next, prev }` (values; `null` = none). Cancelable: `event.preventDefault()` vetoes it (uncontrolled), or in controlled mode answer by updating `value`. |
261
261
  | `onValueChange?` | (event, attrs) => void | — | Like `onChange`, but with a `{ value }` snapshot as the 2nd argument. |
262
262
  | `orientation?` | 'horizontal' \| 'vertical' | 'horizontal' | Layout + arrow-key axis. Horizontal ellipsizes labels when tabs overflow (scroll is opt-in via CSS); vertical stacks them. |
263
- | `priority?` | StepPriority | 'secondary' | Visual emphasis. Primary uses a solid selected marker and a stronger completed connector; secondary is the outlined default; tertiary is compact and borderless. |
263
+ | `priority?` | 'primary' \| 'secondary' \| 'tertiary' | 'secondary' | Visual emphasis. Primary uses a solid selected marker and a stronger completed connector; secondary is the outlined default; tertiary is compact and borderless. |
264
264
  | `renderMarker?` | (option, state) => ReactNode | — | Builds a custom marker from a step and its current state. A returned node replaces `marker` and the built-in state marker; return `undefined` to use those fallbacks, or `null` for an empty ring. |
265
265
  | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size — small 24px · medium 28px · large 32px tall, matching Button's scale (the tab's label leading runs a touch tighter, offset by 1px more block padding per side). |
266
- | `tone?` | StepTone \| '' | 'neutral' | Tone applied to every step without its own `tone`. Error steps stay critical and disabled steps stay neutral. |
266
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| '' | 'neutral' | Tone applied to every step without its own `tone`. Error steps stay critical and disabled steps stay neutral. |
267
267
  | `value?` | string | — | Controlled active value — the tab `value` to mark selected (and, when a `<TabPanel value="…">` shares it, the panel to reveal). When set, you own selection: the strip renders exactly what this says, and a user pick only *requests* a change via `onStateChange` — apply it by updating this prop. Leave undefined (and use `defaultValue`) for uncontrolled. |
268
268
 
269
269
  ## Step option props
@@ -271,14 +271,14 @@ Events match [`Tabs`](./tabs.md#events).
271
271
  | Prop | Type | Default | Description |
272
272
  |------|------|---------|-------------|
273
273
  | `label` | ReactNode | — | Visible phase label. It ellipsizes in a constrained step; its full content is available in a tooltip only when clipped. |
274
- | `state` | StepState | — | Application-owned process state. Selection and availability are separate; `error` keeps a critical icon and outline, and selection adds the fill. |
274
+ | `state` | 'incomplete' \| 'loading' \| 'completed' \| 'error' | — | Application-owned process state. Selection and availability are separate; `error` keeps a critical icon and outline, and selection adds the fill. |
275
275
  | `value` | string | — | Stable phase identity. Values must be unique within the sequence. |
276
276
  | `className?` | string | — | CSS class on the option's rendered row. |
277
277
  | `disabled?` | boolean | — | Disables this phase. A custom `marker` or `renderMarker` result is kept; otherwise it uses the incomplete marker. |
278
278
  | `hint?` | ReactNode | — | Secondary text shown below the label. It ellipsizes in a constrained step; its full content is available in a tooltip only when clipped. |
279
279
  | `marker?` | StepMarker | — | Replaces the marker derived from `state`. A number is shown directly; an Anta icon shape is rendered as an `<Icon>`. |
280
280
  | `style?` | CSSProperties | — | Inline styles on the option's rendered row. |
281
- | `tone?` | StepTone \| '' | inherits Steps `tone | Tone for this phase. It colors the label, hint, marker, and completed connector. Overrides the Steps `tone`; error steps stay critical and disabled steps stay neutral. |
281
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| '' | inherits Steps `tone | Tone for this phase. It colors the label, hint, marker, and completed connector. Overrides the Steps `tone`; error steps stay critical and disabled steps stay neutral. |
282
282
 
283
283
  ## Web Component
284
284
 
@@ -6,6 +6,8 @@ The `Switch` wrapper renders a form-associated `<a-switch>` element. It works un
6
6
 
7
7
  ## Tone
8
8
 
9
+ Switch is neutral by default. Pass `tone="brand"` or another tone to color the control.
10
+
9
11
  `tone` colors the whole control: the checked track, plus the unchecked track border and thumb. Six named tones are available, or pass any CSS color for a custom tone. The label and hint stay neutral.
10
12
 
11
13
  ```tsx
@@ -17,16 +19,16 @@ The `Switch` wrapper renders a form-associated `<a-switch>` element. It works un
17
19
 
18
20
  ## Selected-only tone
19
21
 
20
- `toneSelected` colors the checked track only. The unchecked track and thumb stay neutral until the switch is on. Use it when a resting tinted control could imply a validation state. It accepts the same values as `tone`.
22
+ To apply a tone only while selected, pass `toneScope="selected"` together with `tone`, such as `tone="brand"`. Only the checked track is tinted; the unchecked track border and thumb stay neutral.
21
23
 
22
24
  ```tsx
23
- <Switch toneSelected="brand" label="Off stays neutral" />
24
- <Switch defaultChecked toneSelected="critical" label="Checked track tinted" />
25
+ <Switch tone="brand" toneScope="selected" label="Off stays neutral" />
26
+ <Switch defaultChecked tone="critical" toneScope="selected" label="Checked track tinted" />
25
27
  ```
26
28
 
27
29
  ## Coloring the text
28
30
 
29
- There is no text-tone prop. To tint the label, set `color` on the switch with a theme-aware `--text-N-{tone}` token; to tint the hint, target `a-switch-hint`. Pair either with `tone` or `toneSelected` to color the whole control.
31
+ There is no text-tone prop. To tint the label, set `color` on the switch with a theme-aware `--text-N-{tone}` token; to tint the hint, target `a-switch-hint`. Pair either with `tone` to color the whole control.
30
32
 
31
33
  ```css
32
34
  /* The class is only for the demo — use your own selector. */
@@ -119,7 +121,10 @@ Use **`onStateChange`** to intercept a toggle or drive a controlled switch. Use
119
121
 
120
122
  ## Accessibility
121
123
 
122
- The wrapper sets `role="switch"` and keeps `aria-checked` in sync with the current value. It derives the accessible name from `label` or text children. For a label-less switch, pass `aria-label`.
124
+ The wrapper sets `role="switch"`, and the element keeps `aria-checked` in sync
125
+ with the current value. Its light-DOM label supplies the accessible name and its
126
+ hint supplies the accessible description through `ElementInternals`; no generated
127
+ IDs are needed. For a label-less switch, pass `aria-label`.
123
128
 
124
129
  Space toggles the switch. It follows the [WAI-ARIA switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/).
125
130
 
@@ -130,7 +135,7 @@ Space toggles the switch. It follows the [WAI-ARIA switch pattern](https://www.w
130
135
  | `checked?` | boolean | — | Controlled checked value. In controlled mode, update this in `onStateChange`. |
131
136
  | `defaultChecked?` | boolean | false | Initial checked value for an uncontrolled switch. |
132
137
  | `disabled?` | boolean | — | Disables interaction and removes the switch from the tab order. |
133
- | `hint?` | ReactNode | — | Secondary text rendered under the label. It does not become part of the accessible name. |
138
+ | `hint?` | ReactNode | — | Secondary text rendered under the label and exposed as the switch's accessible description. It does not become part of the accessible name. |
134
139
  | `label?` | string | — | Visible, stable label for the setting. Use `children` for richer label content. |
135
140
  | `labelPosition?` | 'start' \| 'end' | 'end' | Put the visible label before or after the control. Grid layout changes only the visual order, preserving DOM/source order for assistive technologies. |
136
141
  | `name?` | string | — | Form field name. A checked switch submits `value` under this name. |
@@ -139,15 +144,16 @@ Space toggles the switch. It follows the [WAI-ARIA switch pattern](https://www.w
139
144
  | `onValueChange?` | (event, attrs) => void | — | Post-apply callback with the new form-relevant value snapshot. |
140
145
  | `round?` | boolean \| number \| string | — | Fully round the thumb and track. Pass a `number` (px) or CSS length string for a custom track radius; the thumb radius is 3px smaller. |
141
146
  | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size variant. small=26×16px, medium=30×18px, large=34×20px. |
142
- | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'brand' | Color of the track and thumb. A tinted tone also colors the unchecked track border and thumb; use `toneSelected` to color only the checked track. |
143
- | `toneSelected?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'brand' | Like `tone`, but applies only while the switch is checked. The unchecked track and thumb stay neutral. If both are set, `tone` colors the unchecked state and `toneSelected` colors the checked track. |
147
+ | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Color of the track and thumb. A tinted tone also colors the unchecked track border and thumb; use `toneScope="selected"` to color only the checked track. |
148
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply `tone` to every state, or only while checked so the unchecked track and thumb stay neutral. |
144
149
  | `value?` | string | "on" | Value submitted while checked. |
145
150
 
146
151
  ## Web Component
147
152
 
148
153
  Use the web component directly when you are not using React or Preact and a native control does not fit.
149
154
 
150
- The focusable host carries the state. Label and hint are light-DOM children.
155
+ The focusable host carries the state. Label and hint are light-DOM children that
156
+ the element exposes as its accessible name and description.
151
157
 
152
158
  ```html
153
159
  <a-switch role="switch" tabindex="0" name="updates" default-state="checked">
@@ -158,7 +164,7 @@ The focusable host carries the state. Label and hint are light-DOM children.
158
164
 
159
165
  ## Styling
160
166
 
161
- Reach for the props first: **`tone`** colors the checked track plus the unchecked chrome, while **`toneSelected`** colors only the checked track. Both accept any CSS color; **`size`** scales the control, label, and hint. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
167
+ Reach for the props first: **`tone`** chooses the control color, while **`toneScope`** chooses whether it colors all states or only the checked track. `tone` accepts any CSS color; **`size`** scales the control, label, and hint. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
162
168
 
163
169
  ```tsx
164
170
  <Switch tone="#e0457b" defaultChecked label="Custom color" />
@@ -271,15 +271,17 @@ const [tab, setTab] = useState('overview')
271
271
 
272
272
  ## Keyboard & accessibility
273
273
 
274
- Every enabled tab is in the tab order; `Tab` / `Shift`+`Tab` step through them.
275
- The arrow keys also move between enabled tabs (wrapping at the ends), `Home` /
274
+ The selected tab is in the page's tab order. Once focus enters the strip, the
275
+ arrow keys move between enabled tabs (wrapping at the ends), `Home` /
276
276
  `End` jump to the first / last, and `Space` / `Enter` activate the focused tab;
277
277
  arrow / `Home` / `End` navigation activates as it moves (selection follows focus).
278
278
  Disabled tabs are skipped and dropped from the tab order.
279
279
 
280
280
  Pass `label` for the tablist's accessible name. Each `<TabPanel>` names its tab as
281
281
  its accessible label (`aria-labelledby`), set off-DOM so nothing writes the panel's
282
- attributes.
282
+ attributes. Static panel content remains readable without making the panel a tab
283
+ stop. Set `tabIndex={0}` when you want a text-only panel itself to receive focus;
284
+ when the panel starts with a focusable control, Tab reaches that control directly.
283
285
 
284
286
  ## Routing
285
287
 
@@ -357,7 +359,7 @@ so those suit an uncontrolled strip reacting to its own pick. **`onFocus`** /
357
359
  | `value` | string | — | This tab's identity — pairs it with the `<TabPanel value="…">` of the same value, and the value reported by `onStateChange` / `onChange`. Unique per strip. |
358
360
  | `children?` | ReactNode | — | Tab content when you need more than a string — used if `label` is omitted. |
359
361
  | `className?` | string | — | CSS class on the option's rendered row. |
360
- | `disabled?` | boolean | — | Disable just this tab — skipped by keyboard nav and dropped from the tab order (a disabled-but-selected tab stays reachable, per the ARIA pattern). |
362
+ | `disabled?` | boolean | — | Prevent user activation of this tab. A disabled tab can still indicate the current selection. |
361
363
  | `icon?` | IconShape | — | Leading icon shape, rendered before the label. |
362
364
  | `iconTrailing?` | IconShape | — | Trailing icon shape, rendered after the label. |
363
365
  | `label?` | ReactNode | — | Visible label. The string shorthand for the tab's content; for richer content pass `children` instead (`label` wins when both are set). |
@@ -371,17 +373,18 @@ so those suit an uncontrolled strip reacting to its own pick. **`onFocus`** /
371
373
  | Prop | Type | Default | Description |
372
374
  |------|------|---------|-------------|
373
375
  | `value` | string | — | Pairs this panel with the tab (`options` entry) of the same `value`. |
374
- | `children?` | ReactNode | — | Panel content — arbitrary React/Preact. |
375
- | `className?` | string | — | CSS class on the rendered `<a-tabpanel>`. |
376
376
  | `hideMode?` | 'display' \| 'visibility' | display | How this panel hides while inactive: `display` (default — removed from layout and the a11y tree) or `visibility` (keeps its layout box, to measure it or avoid reflow). Both stay mounted; to *not render* an inactive panel, render it conditionally off a controlled `value` (see the Tabs docs). |
377
- | `style?` | CSSProperties | — | Inline style on the rendered `<a-tabpanel>`. |
378
377
 
379
378
  ## Web Component
380
379
 
381
380
  Use the web component directly when you are not using React or Preact and a native control does not fit.
382
381
 
383
382
  Keep the tab strip and panels as siblings under one parent. Panels read the active
384
- value from `<a-tabs>`.
383
+ value from `<a-tabs>`. Give the selected tab `tabindex="0"` and the others
384
+ `tabindex="-1"`; `<a-tabs>` moves focus and selection with the arrow keys, while
385
+ your change handler updates those attributes for the next visit to the strip. Add
386
+ `tabindex="0"` to a text-only panel only when you want the panel itself in the page
387
+ Tab sequence.
385
388
 
386
389
  Add `fill` to make horizontal tabs share the available width equally.
387
390
 
@@ -396,6 +399,16 @@ Add `fill` to make horizontal tabs share the available width equally.
396
399
  </div>
397
400
  ```
398
401
 
402
+ ```js title="Keep the raw DOM tab stop on the selected tab"
403
+ const tabs = document.querySelector('a-tabs')
404
+
405
+ tabs.addEventListener('change', () => {
406
+ for (const tab of tabs.querySelectorAll('a-tab')) {
407
+ tab.tabIndex = tab.getAttribute('value') === tabs.value ? 0 : -1
408
+ }
409
+ })
410
+ ```
411
+
399
412
  ## Styling
400
413
 
401
414
  Reach for the props first: **`tone`**, **`size`**, **`priority`**. The focus ring is