@antadesign/anta 0.3.34 → 0.3.35

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 (184) hide show
  1. package/LICENSE.md +14 -1
  2. package/README.md +9 -0
  3. package/dist/anta_helpers.d.ts +46 -30
  4. package/dist/anta_helpers.js +66 -25
  5. package/dist/bundle.css +1 -1
  6. package/dist/bundle.js +2 -1333
  7. package/dist/components/Banner.d.ts +25 -40
  8. package/dist/components/Banner.js +15 -17
  9. package/dist/components/Button.js +1 -0
  10. package/dist/components/ButtonCopy.js +3 -2
  11. package/dist/components/Calendar.js +13 -13
  12. package/dist/components/Card.d.ts +9 -17
  13. package/dist/components/Card.js +50 -46
  14. package/dist/components/Dialog.d.ts +1 -0
  15. package/dist/components/Dialog.js +16 -10
  16. package/dist/components/Expander.d.ts +6 -21
  17. package/dist/components/Expander.js +24 -23
  18. package/dist/components/Icon.d.ts +8 -14
  19. package/dist/components/Input.d.ts +77 -96
  20. package/dist/components/Input.js +23 -27
  21. package/dist/components/InputAutocomplete.d.ts +29 -10
  22. package/dist/components/InputAutocomplete.js +25 -8
  23. package/dist/components/InputDate.js +2 -2
  24. package/dist/components/InputTime.d.ts +10 -4
  25. package/dist/components/InputTime.js +20 -16
  26. package/dist/components/Loader.d.ts +2 -2
  27. package/dist/components/Menu.d.ts +5 -4
  28. package/dist/components/Menu.js +13 -8
  29. package/dist/components/MenuItem.js +1 -14
  30. package/dist/components/MenuItemCopy.js +2 -2
  31. package/dist/components/Panel.d.ts +3 -1
  32. package/dist/components/Panel.js +17 -11
  33. package/dist/components/Progress.d.ts +21 -23
  34. package/dist/components/Progress.js +13 -9
  35. package/dist/components/RadioGroup.js +6 -8
  36. package/dist/components/{select-parts.css → Select.css} +1 -1
  37. package/dist/components/Select.d.ts +53 -35
  38. package/dist/components/Select.js +53 -97
  39. package/dist/components/SelectFaceted.d.ts +7 -3
  40. package/dist/components/SelectFaceted.js +21 -9
  41. package/dist/components/Slider.d.ts +28 -11
  42. package/dist/components/Slider.js +20 -20
  43. package/dist/components/Steps.css +1 -1
  44. package/dist/components/Steps.js +1 -11
  45. package/dist/components/Tabs.d.ts +17 -20
  46. package/dist/components/Tabs.js +5 -8
  47. package/dist/components/Text.d.ts +55 -36
  48. package/dist/components/Text.js +35 -22
  49. package/dist/components/Title.d.ts +16 -26
  50. package/dist/components/Title.js +2 -1
  51. package/dist/components/Toaster.js +9 -2
  52. package/dist/components/Tooltip.d.ts +10 -15
  53. package/dist/components/copy-props.d.ts +14 -3
  54. package/dist/components/copy-props.js +14 -3
  55. package/dist/components/select-tree.d.ts +24 -0
  56. package/dist/components/select-tree.js +35 -0
  57. package/dist/element-base.d.ts +10 -0
  58. package/dist/element-base.js +15 -0
  59. package/dist/elements/a-banner.css +1 -1
  60. package/dist/elements/a-banner.d.ts +4 -1
  61. package/dist/elements/a-banner.js +22 -112
  62. package/dist/elements/a-button.d.ts +2 -1
  63. package/dist/elements/a-button.js +18 -1
  64. package/dist/elements/a-calendar.d.ts +14 -13
  65. package/dist/elements/a-calendar.js +29 -24
  66. package/dist/elements/a-card.css +1 -1
  67. package/dist/elements/a-card.d.ts +0 -9
  68. package/dist/elements/a-card.js +0 -184
  69. package/dist/elements/a-checkbox.css +1 -1
  70. package/dist/elements/a-copy.css +1 -1
  71. package/dist/elements/a-copy.d.ts +1 -5
  72. package/dist/elements/a-copy.js +129 -108
  73. package/dist/elements/a-dialog.css +1 -1
  74. package/dist/elements/a-dialog.d.ts +1 -1
  75. package/dist/elements/a-dialog.js +66 -194
  76. package/dist/elements/a-expander.css +1 -1
  77. package/dist/elements/a-expander.d.ts +4 -30
  78. package/dist/elements/a-expander.js +85 -344
  79. package/dist/elements/a-icon.d.ts +5 -23
  80. package/dist/elements/a-icon.js +1 -2
  81. package/dist/elements/a-input-time.css +1 -1
  82. package/dist/elements/a-input-time.d.ts +2 -1
  83. package/dist/elements/a-input-time.js +101 -168
  84. package/dist/elements/a-input.css +1 -1
  85. package/dist/elements/a-input.d.ts +10 -8
  86. package/dist/elements/a-input.js +155 -230
  87. package/dist/elements/a-loader.d.ts +5 -8
  88. package/dist/elements/a-loader.js +1 -2
  89. package/dist/elements/a-menu-item.css +1 -1
  90. package/dist/elements/a-menu-item.d.ts +5 -3
  91. package/dist/elements/a-menu-item.js +5 -1
  92. package/dist/elements/a-menu.css +1 -1
  93. package/dist/elements/a-menu.d.ts +5 -7
  94. package/dist/elements/a-menu.js +383 -299
  95. package/dist/elements/a-panel.css +1 -1
  96. package/dist/elements/a-panel.js +19 -37
  97. package/dist/elements/a-progress.css +1 -1
  98. package/dist/elements/a-progress.d.ts +0 -22
  99. package/dist/elements/a-progress.js +0 -100
  100. package/dist/elements/a-radio-group.d.ts +1 -1
  101. package/dist/elements/a-radio-group.js +6 -4
  102. package/dist/elements/a-select-faceted.d.ts +1 -1
  103. package/dist/elements/a-select-faceted.js +1 -1
  104. package/dist/elements/a-select.d.ts +1 -1
  105. package/dist/elements/a-select.js +1 -1
  106. package/dist/elements/a-slider.css +1 -1
  107. package/dist/elements/a-slider.d.ts +11 -3
  108. package/dist/elements/a-slider.js +244 -242
  109. package/dist/elements/a-switch.css +1 -1
  110. package/dist/elements/a-tab.css +1 -1
  111. package/dist/elements/a-tabs.css +1 -1
  112. package/dist/elements/a-tabs.d.ts +1 -1
  113. package/dist/elements/a-tabs.js +6 -4
  114. package/dist/elements/a-tag.css +1 -1
  115. package/dist/elements/a-text.css +1 -1
  116. package/dist/elements/a-text.d.ts +6 -43
  117. package/dist/elements/a-text.js +110 -193
  118. package/dist/elements/a-title.css +1 -1
  119. package/dist/elements/a-toast.css +1 -1
  120. package/dist/elements/a-toast.d.ts +10 -45
  121. package/dist/elements/a-toast.js +80 -66
  122. package/dist/elements/a-toaster.css +1 -1
  123. package/dist/elements/a-toaster.js +9 -44
  124. package/dist/elements/a-tooltip.css +1 -1
  125. package/dist/elements/a-tooltip.d.ts +1 -1
  126. package/dist/elements/a-tooltip.js +142 -136
  127. package/dist/elements/copy-behavior.d.ts +1 -1
  128. package/dist/elements/copy-behavior.js +21 -21
  129. package/dist/elements/index.d.ts +3 -3
  130. package/dist/elements/index.js +3 -7
  131. package/dist/elements/input-focus-modality.d.ts +15 -0
  132. package/dist/elements/input-focus-modality.js +44 -0
  133. package/dist/elements/input-trailing-actions.d.ts +5 -0
  134. package/dist/elements/input-trailing-actions.js +38 -0
  135. package/dist/elements/shadow-aria.d.ts +2 -0
  136. package/dist/elements/shadow-aria.js +2 -1
  137. package/dist/elements/value-input.d.ts +3 -0
  138. package/dist/elements/value-input.js +12 -0
  139. package/dist/general_types.d.ts +103 -96
  140. package/dist/index.d.ts +1 -1
  141. package/dist/input-events.d.ts +4 -0
  142. package/dist/input-events.js +15 -0
  143. package/dist/jsx-runtime.d.ts +70 -23
  144. package/dist/jsx-runtime.js +26 -19
  145. package/dist/native-event-props.d.ts +2 -0
  146. package/dist/native-event-props.js +10 -0
  147. package/dist/reset.css +1 -1
  148. package/dist/theme-antithesis.css +10 -1
  149. package/dist/theme-antune.css +1 -1
  150. package/docs/accessibility.md +17 -3
  151. package/docs/changelog.md +348 -2
  152. package/docs/comparison.md +2 -2
  153. package/docs/components/avatar.md +5 -0
  154. package/docs/components/banner.md +117 -187
  155. package/docs/components/button.md +22 -0
  156. package/docs/components/card.md +74 -28
  157. package/docs/components/checkbox.md +9 -10
  158. package/docs/components/dialog.md +83 -32
  159. package/docs/components/expander.md +198 -435
  160. package/docs/components/icon.md +11 -17
  161. package/docs/components/input-autocomplete.md +152 -52
  162. package/docs/components/input-date.md +175 -148
  163. package/docs/components/input-time.md +81 -38
  164. package/docs/components/input.md +343 -349
  165. package/docs/components/loader.md +2 -1
  166. package/docs/components/menu.md +35 -6
  167. package/docs/components/panel.md +18 -0
  168. package/docs/components/progress.md +83 -49
  169. package/docs/components/radio.md +39 -43
  170. package/docs/components/select-faceted.md +54 -7
  171. package/docs/components/select.md +390 -386
  172. package/docs/components/slider.md +136 -19
  173. package/docs/components/steps.md +2 -1
  174. package/docs/components/switch.md +8 -9
  175. package/docs/components/tabs.md +110 -104
  176. package/docs/components/text.md +110 -86
  177. package/docs/components/title.md +47 -33
  178. package/docs/components/toaster.md +242 -84
  179. package/docs/components/tooltip.md +204 -113
  180. package/docs/install-config.md +183 -26
  181. package/docs/packages/plot.md +170 -148
  182. package/docs/theming.md +7 -0
  183. package/package.json +41 -3
  184. package/theme-antithesis-license.md +66 -0
@@ -1,13 +1,13 @@
1
1
  # Select
2
2
 
3
- `Select` lets people choose one or more options from a dropdown. Its read-only
4
- field shows the selected value. Click the field to open the menu. The menu
5
- closes when a user clicks outside it or presses Esc.
3
+ `Select` lets people choose one or more options from a dropdown. Use it to
4
+ choose a setting, filter a view, or configure a list. Add search for longer
5
+ lists, and use groups or submenus to organize related options.
6
6
 
7
- Select uses an [Anta Button](./button.md) for its field and a [Menu](./menu.md) for its
8
- dropdown. Pass `options` as an array of strings or objects. Use `value` with
9
- **`onValueChange`** to control the selection, or use `defaultValue` for an
10
- uncontrolled Select.
7
+ Give the field a visible `label`. If you omit it, provide `aria-label` or
8
+ `aria-labelledby` so people using assistive technology can identify the control.
9
+ Include any meaningful prefix in that name; leading content and the chevron
10
+ are decorative.
11
11
 
12
12
  ## Options
13
13
 
@@ -25,202 +25,227 @@ uncontrolled Select.
25
25
  />
26
26
  ```
27
27
 
28
- `options` takes bare strings or objects. A string is shorthand for
29
- `{ value, label: value }`. An object adds a `label`, a `hint` (the secondary line),
30
- a leading `icon`, a per-option `tone` (a named tone or a custom CSS color), or
31
- `disabled`. Open the field: the chosen row is highlighted, and the disabled row is
32
- skipped by the keyboard.
28
+ Use strings for a simple list, or option objects when people need more context.
29
+ Add a `label` for a readable name, a `hint` for supporting information, or an
30
+ `icon` to help distinguish choices. Use `tone` to communicate a meaningful
31
+ status alongside the text.
32
+
33
+ Set `disabled` on an unavailable option to keep it visible without allowing
34
+ selection. Disabled options are also skipped during keyboard navigation.
35
+
36
+ ### Groups and submenus
37
+
38
+ ```tsx
39
+ // The same tree drives both modes — selection is global, so `selection` is the
40
+ // only difference between the two selects below.
41
+ const [one, setOne] = useState<string | null>('stream')
42
+ const [many, setMany] = useState(['stream', 'tag.env'])
43
+ const fields = [
44
+ 'output_text', // a plain option
45
+ { label: 'Log', options: ['stream', 'message', 'severity'] }, // an inline group
46
+ {
47
+ label: 'Metadata', // a submenu (flyout)
48
+ icon: 'braces',
49
+ submenu: [
50
+ 'metadata.host',
51
+ 'metadata.region',
52
+ { label: 'Tags', options: ['tag.env', 'tag.team'] }, // group inside submenu
53
+ { label: 'Custom', submenu: ['custom.a', 'custom.b'] }, // submenu inside submenu
54
+ ],
55
+ },
56
+ { label: 'Time', submenu: ['vtime', 'wall_clock'], disabled: true }, // disabled → cascades
57
+ ]
58
+
59
+ <Select label="Field · single" filter options={fields} value={one} onValueChange={setOne} />
60
+
61
+ <Select label="Fields · multiple" selection="multiple" filter options={fields} value={many} onValueChange={setMany} />
62
+ ```
63
+
64
+ Use groups to divide a list into named sections. Use submenus when a category
65
+ needs a separate list of choices. Both can contain further groups or submenus.
66
+
67
+ The examples use the same options with single and multiple selection. Group
68
+ headings and submenu parents organize the choices; only their options are
69
+ selectable. Keep option values unique across the whole tree. Disabling a group
70
+ or submenu disables its descendants.
71
+
72
+ When someone searches, matching options appear together in a flat list with
73
+ group headings. They can find a match without opening each submenu.
33
74
 
34
75
  ## Selection modes
35
76
 
36
77
  ```tsx
37
- // Single-select marks (single value)
38
- <Select indicator="check" options={OPTIONS} value={one} onValueChange={setOne} /> // trailing ✓ + tint
39
- <Select indicator="radio" options={OPTIONS} value={one} onValueChange={setOne} /> // leading radio
40
- <Select options={OPTIONS} value={one} onValueChange={setOne} /> // tint only (default)
78
+ const options = ['output_text', 'stream', 'container', 'vtime', 'custom']
79
+ const [check, setCheck] = useState<string | null>('stream')
80
+ const [radio, setRadio] = useState<string | null>('stream')
81
+ const [many, setMany] = useState(['stream', 'container'])
41
82
 
42
- // Tone the selected row: named tone or custom color (the indicator adopts it)
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} />
83
+ <Select label="Checkmark · brand tone" indicator="check" tone="brand"
84
+ toneScope="selected" options={options} value={check} onValueChange={setCheck} />
45
85
 
46
- // Multiple: checkboxes, count summary, Select-all row (default); value is an array
47
- <Select
48
- selection="multiple"
49
- options={OPTIONS}
50
- value={many}
51
- onValueChange={setMany}
52
- />
86
+ <Select label="Radio · custom tone" indicator="radio" tone="#c026d3"
87
+ toneScope="selected" options={options} value={radio} onValueChange={setRadio} />
88
+
89
+ <Select label="Multiple · brand tone" selection="multiple" tone="brand"
90
+ toneScope="selected" placeholder="Select fields…"
91
+ options={options} value={many} onValueChange={setMany} />
53
92
  ```
54
93
 
55
- `selection` sets **behavior**, `indicator` sets the single-select **mark**.
56
-
57
- `selection="single"` (the default) keeps `value` a single value and closes on pick.
58
- `indicator` then picks the row mark: **`"none"`** (default; a tint-only highlight),
59
- **`"check"`** (a trailing checkmark on the chosen row that keeps the tint, the
60
- canonical Select look), or **`"radio"`** (a leading radio on every row).
61
-
62
- `selection="multiple"` makes `value` an **array**, puts a **checkbox** on
63
- every row, keeps the menu open as you toggle, and summarises the selection in the
64
- field: **`All`** when every option is on, the single label for one, else **"N
65
- selected"** (pass **`verbose`** to spell the picks out — `3 selected: A, B, C`; or
66
- take over the text entirely with [`renderSummary`](#selection-summary); an empty
67
- selection shows the `placeholder`). A
68
- **Select all** top row shows by default; it toggles everything, and
69
- its box goes mixed when only some are on. Pass **`selectAll={false}`** to drop it.
70
-
71
- In a multi-select, **`Alt`+Click** a row to clear every other selection and
72
- select that row. On macOS, use Option instead of Alt.
73
- After hovering for 700ms, the standard tooltip describes this shortcut beside
74
- the row. Moving to another row restarts the delay.
75
- Set `SelectOption.tooltip` to replace it, or to `''` to hide it. The shortcut
76
- is available only when `selectAll` is enabled, so `selectAll={false}` removes it.
77
-
78
- Each checkable row is the control itself (`role="menuitemcheckbox"` /
79
- `menuitemradio`, `aria-checked`); the checkbox / radio / check is a passive indicator
80
- the row drives.
81
-
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
85
- `indicator` `none` / `check`, which show the row tint; `radio` / `checkbox` have no
86
- row tint, so it tones the label and indicator only.
87
-
88
- ## Filter
94
+ Use single selection when someone must choose one option, such as a default
95
+ field. Choosing an option closes the menu. Use `selection="multiple"` for a
96
+ set of choices, such as the fields shown in a table. The menu stays open so
97
+ people can continue selecting.
98
+
99
+ Multiple selection shows checkboxes and summarizes the chosen options in the
100
+ field. Use `verbose` to list their labels or customize the
101
+ [selection summary](#selection-summary).
102
+
103
+ ### Indicators and selection colors
104
+
105
+ For single selection, choose the mark that suits the list: `indicator="none"`
106
+ highlights the chosen row, `"check"` adds a checkmark, and `"radio"` shows a
107
+ radio indicator beside every option. Multiple selection always uses checkboxes.
108
+
109
+ Use `tone` with `toneScope="selected"` to give chosen options a shared color.
110
+ Named tones and custom CSS colors are supported. Tint-based indicators color
111
+ the row background; radio and checkbox indicators color the text and mark.
112
+
113
+ ### Select all and selection shortcuts
114
+
115
+ Multiple selection includes a **Select all** row. It selects or clears enabled
116
+ options and shows a mixed checkbox when only some are selected. With an active
117
+ search, it affects only matching options. Set `selectAll={false}` to remove it.
118
+
119
+ Alt-click an option to select it and clear every other choice. On macOS, use
120
+ Option instead of Alt. The default row tooltip explains
121
+ this shortcut. Set an option's `tooltip` to replace the hint, or to `''` to hide
122
+ it. The shortcut is available only while `selectAll` is enabled.
123
+
124
+ <a id="filter"></a>
125
+
126
+ ## Filtering options
89
127
 
90
128
  ```tsx
91
- // Built-in matcher: case-insensitive substring of value / label / hint
92
- <Select filter indicator="check" options={OPTIONS} value={v} onValueChange={setV} />
129
+ const options = [
130
+ { value: 'output_text', hint: 'Raw log message or event payload' },
131
+ { value: 'stream', hint: 'Log level: error · info · internal' },
132
+ { value: 'container', hint: 'Host where the event occurred' },
133
+ { value: 'vtime', hint: 'Simulation time in seconds' },
134
+ { value: 'source', hint: 'Emitting system or process' },
135
+ { value: 'message', hint: 'Assertion description' },
136
+ { value: 'condition', hint: 'Passing or failing' },
137
+ { value: 'fault', hint: 'Injected fault type and name' },
138
+ ]
139
+ const [one, setOne] = useState<string | null>('stream')
140
+ const [many, setMany] = useState(['stream'])
93
141
 
94
- // Custom matcher: a per-option predicate
95
- <Select
96
- filter={(option, query) => option.value.startsWith(query)}
97
- options={OPTIONS}
98
- value={v}
99
- onValueChange={setV}
100
- />
142
+ <Select label="Single · filter" filter indicator="check"
143
+ options={options} value={one} onValueChange={setOne} />
144
+
145
+ <Select label="Multiple · filter" filter selection="multiple"
146
+ placeholder="Fields…" options={options} value={many} onValueChange={setMany} />
147
+ ```
148
+
149
+ For prefix matching instead of the built-in search:
150
+
151
+ ```tsx
152
+ <Select label="Field" options={options}
153
+ filter={(option, query) => String(option.value).startsWith(query)} />
101
154
  ```
102
155
 
103
- Add **`filter`** for a search field at the top of the menu that narrows the options
104
- as you type. `true` uses the built-in matcher: a case-insensitive substring of the
105
- option's **value, label, or hint**. It's **whitespace-flexible** (a typed space
106
- matches any run of whitespace) and **bolds the matched substring** in the results.
107
- Pass a **function** `(option, query) => boolean` for custom matching (no highlight).
156
+ Add `filter` when people need to find an option in a longer list. Search matches
157
+ option values, labels, and hints without requiring the same capitalization.
158
+ Matching text is highlighted in the built-in option layout.
159
+
160
+ Pass a function to `filter` when your application needs different matching rules.
161
+ Custom matching does not highlight text. Use `renderEmpty` to explain when no
162
+ options match; see [Empty and loading states](#empty-and-loading-states).
163
+
164
+ ### Keyboard interaction
108
165
 
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.
166
+ Opening a searchable Select focuses the search field. Press ↑ or
167
+ ↓ to move through matching options, then Enter to select
168
+ the active option. Focus stays in the search field while you navigate.
117
169
 
118
- ## Value and changes
170
+ The search field stays visible while results scroll. Press Esc or
171
+ select outside the popup to dismiss it. Closing the popup resets the query.
172
+
173
+ <a id="value-and-changes"></a>
174
+
175
+ ## Managing the value
119
176
 
120
177
  ```tsx
121
- const [field, setField] = useState('stream')
178
+ const [field, setField] = useState<string | null>('stream')
122
179
 
123
180
  <Select
124
181
  label="Field"
125
182
  options={['output_text', 'stream', 'container', 'vtime', 'custom']}
183
+ placeholder="Select a field…"
126
184
  value={field}
127
- onValueChange={(v) => setField(v)}
185
+ onValueChange={setField}
186
+ clearable
128
187
  />
188
+ <Button label="Reset selection" size="small" priority="tertiary"
189
+ onClick={() => setField(null)} />
129
190
  ```
130
191
 
131
- The value is the **option's `value`** — `value="stream"` in single mode, an array
132
- like `value={['stream', 'container']}` in `multiple`. It's the option's identity:
133
- what `value` / `defaultValue` name and what `onValueChange` reports, so keep each
134
- option's `value` **unique across the whole tree**. Selection is value-keyed and
135
- global across groups / submenus, so a value repeated in two sections is one logical
136
- pick (both rows toggle together; the trigger resolves to the last). Dev builds
137
- `console.warn` on a duplicate.
138
-
139
- Values aren't limited to strings. `Select` **infers its value type from `options`**:
140
- pass `options={[{ value: 365 }, { value: 90 }]}` and `value`, `onValueChange`'s value,
141
- and `attrs.value` are all `number` (the same for `boolean`). Values compare with `===`,
142
- so `365` and `"365"` stay distinct and no string conversion is needed. The exported
143
- `OptionValue` type is `string | number | boolean`; for anything richer (a date, an
144
- object), give `value` a **stable primitive key** (a date's ISO string, a record's id)
145
- and read the full value back off `attrs.option`.
192
+ Pass `value` and update it in `onValueChange` when your application manages the
193
+ selection. Set it to `null` to clear a single selection while keeping control.
194
+ The example's **Reset selection** button does this from application state.
146
195
 
147
- ```tsx
148
- // Use the ID as the value and attach the full record to the option.
149
- <Select
150
- options={runs.map((r) => ({ value: r.id, label: r.name, run: r }))}
151
- onValueChange={(id, attrs) => { if (!('all' in attrs)) openRun(attrs.option.run) }}
152
- />
196
+ For a Select that manages its own selection, omit `value` or pass `undefined`.
197
+ Use `defaultValue` to choose an initial option. Clear reports `null` for single
198
+ selection and `[]` for multiple selection.
199
+
200
+ Single selection uses one option value. Multiple selection uses an array of
201
+ values. Keep each option's value unique across groups and submenus.
202
+
203
+ Values can be strings, numbers, or booleans. Select preserves their types, so a
204
+ numeric ID can remain a number in your application state. Use a stable primitive
205
+ key for richer data, and attach the record to its option when you need it in a
206
+ callback:
153
207
 
154
- // Use an ISO string as the value, then convert it back to a Date in the callback.
208
+ ```tsx
155
209
  <Select
156
- options={days.map((d) => ({ value: d.toISOString(), label: fmt(d) }))}
157
- onValueChange={(iso) => setDay(new Date(iso))}
210
+ label="Run"
211
+ options={runs.map((run) => ({ value: run.id, label: run.name, run }))}
212
+ onValueChange={(id, attrs) => {
213
+ if (!('all' in attrs)) openRun(attrs.option.run)
214
+ }}
158
215
  />
159
216
  ```
160
217
 
161
- For an **uncontrolled** Select, omit `value` and pass `defaultValue`. For a
162
- **controlled** Select, pass `value` and update it in `onValueChange`. The field
163
- then shows the value supplied by the application.
218
+ For dates, use an ISO string as the option value and convert it back in the
219
+ callback. The API reference describes the additional change information passed
220
+ to `onValueChange`.
164
221
 
165
- **`onValueChange(value, attrs)`** is the single selection callback. The new value
166
- comes first (a single value, or an array in `multiple` mode), with a snapshot
167
- second: `{ value, option }` for the changed row, plus `selected` in `multiple` mode
168
- (or `{ all: true }` for the Select-all row). There's **no `onStateChange`**: Select
169
- has no discrete element state to veto (Checkbox and RadioGroup do), so it follows
170
- Input's model of one value callback, and a controlled consumer rejects a pick by not
171
- updating `value`. See the [event-model note on Input](./input.md#events) for
172
- the rationale.
222
+ <a id="deriving-the-selection-tree"></a>
173
223
 
174
- ### Deriving the selection tree
224
+ ### Building a grouped selection summary
175
225
 
176
- The callback hands you the new value (and the one row that changed); to read the
177
- whole selection back against your hierarchy, use **`optionsWithSelection(options,
178
- values)`**. It returns your `options` tree with every leaf marked `selected` and
179
- every group / submenu carrying a rolled-up `selectionState` — `'all'`, `'none'`, or
180
- `'some'` of its descendants. It's a pure function (no `Select` instance, reads
181
- nothing off the event), so it behaves the same controlled or uncontrolled: hand it
182
- your current `value` and render a grouped summary, a section indicator, or a diff.
226
+ Use `optionsWithSelection(options, values)` to build a grouped summary or show
227
+ how much of each section is selected. It returns your options tree annotated
228
+ with selected leaves and each group's `selectionState`: `all`, `some`, or `none`.
183
229
 
184
- The panel on the right is that projection, re-derived on every pick. Toggle scopes and
185
- watch the leaf checks and the section `Tag`s (`some` / `all`) update:
230
+ The example displays a summary alongside the Select. Choose permissions and
231
+ watch the section indicators update.
186
232
 
187
233
  ```tsx
188
234
  import { Select, optionsWithSelection } from '@antadesign/anta'
189
235
 
190
- const [values, setValues] = useState<string[]>(['repo.read', 'billing.view'])
236
+ const [values, setValues] = useState<string[]>(['repo.read', 'billing.view', 'billing.manage'])
191
237
 
192
238
  // `values` drives the Select; the tree is derived from it and rendered read-only.
193
239
  const tree = optionsWithSelection(scopes, values)
194
- // tree: [ { label: 'Repositories', selectionState: 'some', options: [
195
- // { value: 'repo.read', label: 'Read', selected: true },
196
- // { value: 'repo.write', label: 'Write', selected: false }, … ] },
197
- // { label: 'Members', selectionState: 'some', options: [ …,
198
- // { label: 'Billing', selectionState: 'all', submenu: [
199
- // { value: 'billing.view', label: 'View invoices', selected: true },
200
- // { value: 'billing.manage', label: 'Manage plan', selected: true } ] } ] },
201
- // { value: 'audit.read', label: 'Audit log', selected: false } ]
202
-
203
- <Select selection="multiple" options={scopes} value={values} onValueChange={setValues} />
204
- {/* render `tree` however you like — a summary, section indicators, a diff */}
205
- ```
206
240
 
207
- **Don't feed the tree back into `options` to control selection.** The projection
208
- flows one way: `value` controls the `Select`, and the tree is a derived, read-only
209
- view of it. `Select` resolves selection purely from `value` and never reads a
210
- `selected` flag off `options`, so a `selected: true` in the tree changes nothing — it
211
- rides along as inert data. Control the `Select` with the value array; keep the
212
- annotated tree for rendering.
213
-
214
- ```tsx
215
- // ✗ Wrong — `selected` is ignored, so the Select isn't controlled by this
216
- <Select options={optionsWithSelection(scopes, values)} />
217
-
218
- // ✓ Right — selection is value-keyed; drive it with `value`
219
- <Select options={scopes} value={values} onValueChange={setValues} />
241
+ <Select label="Scopes" selection="multiple" filter placeholder="Select scopes…"
242
+ options={scopes} value={values} onValueChange={setValues} />
243
+ {/* render `tree` however you like — a summary, section indicators, a diff */}
220
244
  ```
221
245
 
222
- Bare-string options normalize to `{ value, label }`; a value under more than one
223
- section marks the leaf everywhere it occurs (selection is value-keyed).
246
+ Keep `value` as the source of selection. The annotated tree is a derived view
247
+ for rendering; passing it back as `options` does not control which rows are
248
+ selected.
224
249
 
225
250
  ## Size and status
226
251
 
@@ -229,93 +254,44 @@ section marks the leaf everywhere it occurs (selection is value-keyed).
229
254
  <Select label="Medium · success" size="medium" status="success" hint="Field is valid" options={OPTIONS} defaultValue="stream" />
230
255
  <Select label="Large · critical" size="large" status="critical" hint="Pick a field" options={OPTIONS} placeholder="Required…" />
231
256
  {/* statusIcon overrides the per-status glyph, or `false` drops it, as on Input */}
232
- <Select status="info" statusIcon="sparkles" hint="Custom glyph" options={OPTIONS} defaultValue="stream" />
257
+ <Select label="Small · custom icon" size="small" status="info" statusIcon="sparkles" hint="statusIcon override" options={OPTIONS} defaultValue="stream" />
233
258
  ```
234
259
 
235
- The trigger is an [Anta Button](./button.md) styled as a field.
236
- **`size`** (`small` · `medium` · `large`) sets the height and type scale.
237
- **`status`** paints a validation tone on the border, `hint`, and chevron, and
238
- prefixes a glyph (`info` / `success` / `warning` / `critical`); **`statusIcon`**
239
- overrides that glyph (any shape, or `false` to drop it), as on Input. The message
240
- rides in `hint`.
260
+ Choose `size` to fit the surrounding controls. Use `status` and a helpful `hint`
261
+ to explain validation or feedback. `statusIcon` lets you replace the status icon
262
+ or hide it with `false`.
263
+
264
+ A `critical` status also marks the control as invalid for assistive technology.
265
+ Use `disabled` when the whole Select is unavailable.
266
+
267
+ ### Using Select in forms
241
268
 
242
- `status` is a validation *state* over the same palette as `tone`: it also carries
243
- the glyph and, for `critical`, validity. See the
244
- [Input status note](./input.md#status). The field also takes `label`,
245
- `placeholder`, `round`, and `disabled`, and fills its container; set a width via
246
- `style` / `className` (both forwarded to the field).
269
+ Select reports choices through `onValueChange`. It does not submit a form value
270
+ or block submission. Include the selected value in your application's form
271
+ handling, and validate it before accepting the submission.
272
+
273
+ <a id="build-it-from-elements"></a>
247
274
 
248
275
  ## Customization
249
276
 
250
- Select has no `<a-select>` element. The React and Preact wrapper coordinates an
251
- Anta Button and a Menu. You can use the wrapper, render custom option content with
252
- `renderOption`, replace its selection mark with `renderIndicator`, or build the
253
- same interaction from the raw elements.
277
+ Customize the option content, selection indicators, summary, or trigger to suit
278
+ the task. Select coordinates an [Input](./input.md) field and a [Menu](./menu.md):
279
+ Input provides the field, label, and hint; Menu provides positioning, dismissal,
280
+ and keyboard navigation. Select keeps their displayed selection in sync with
281
+ the value.
254
282
 
255
- Pass `placement` to control where the options menu opens relative to its field. It
256
- uses the same values as `Menu` and still flips or clamps when space is limited.
257
- `offset` sets the gap in pixels between the field and the menu:
283
+ Use `placement` when the menu should open on a particular side of the trigger.
284
+ It adjusts to the available space. Set `offset` to change the gap:
258
285
 
259
286
  ```tsx
260
287
  <Select placement="top-end" offset={8} options={OPTIONS} label="Field" />
261
288
  ```
262
289
 
263
- ### Build it from elements
290
+ For field and menu appearance, use the supported hooks in [Styling](#styling).
264
291
 
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
267
- and each option's `selected` state from the value, and reports a pick through
268
- `onValueChange`. Menu handles positioning, dismissal, and keyboard navigation.
292
+ <a id="custom-option-rendering"></a>
269
293
 
270
- The following example shows the equivalent markup without React or Preact. Place
271
- the Menu directly after the Button so it can use that Button as its trigger.
272
-
273
- ```html
274
- <!-- `.select` scopes this example's CSS. Use an application selector instead. -->
275
- <div class="select" style="width: 240px">
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>
280
- <a-menu role="menu">
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>
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>
283
- <a-menu-item role="menuitemradio" tabindex="0" aria-checked="false" value="container"><a-menu-item-label>container</a-menu-item-label></a-menu-item>
284
- </a-menu>
285
- </div>
286
- <script type="module">
287
- import '@antadesign/anta/elements'
288
- const root = document.querySelector('.select')
289
- const field = root.querySelector('a-button')
290
- const menu = root.querySelector('a-menu')
291
- const chevron = field.querySelector('a-icon')
292
- // Flip the chevron with the menu's open state (as <Select> does).
293
- menu.addEventListener('statechange', (e) => {
294
- const open = e.detail.next === 'open'
295
- field.setAttribute('aria-expanded', String(open))
296
- chevron.style.transform = open ? 'rotate(180deg)' : ''
297
- })
298
- // On pick: mark the row selected + reflect its label into the field.
299
- menu.addEventListener('click', (e) => {
300
- const item = e.target.closest('a-menu-item')
301
- if (!item) return
302
- menu.querySelectorAll('a-menu-item').forEach((el) => {
303
- const selected = el === item
304
- el.toggleAttribute('selected', selected)
305
- el.setAttribute('aria-checked', String(selected))
306
- })
307
- field.querySelector('a-button-label').textContent = item.querySelector('a-menu-item-label').textContent
308
- })
309
- </script>
310
- ```
311
-
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.
317
-
318
- ### Custom option rendering
294
+ ### Custom option content
319
295
 
320
296
  ```tsx
321
297
  const TONE = { in_progress: 'info', completed: 'neutral', incomplete: 'critical' }
@@ -351,24 +327,22 @@ const LABEL = { in_progress: 'In progress', completed: 'Completed', incomplete:
351
327
  </div>
352
328
  ```
353
329
 
354
- **`renderOption(option, state)`** replaces the built-in `label`, `hint`, and
355
- `icon` layout for a row. Select still renders the row container, click behavior,
356
- ARIA attributes, and selection indicator. Set each option's `label` for the
357
- closed field text.
358
-
359
- Attach your own fields to each option (`SelectOption` carries an index signature) and
360
- read them back off `option`; those extra fields are typed `unknown`, so cast once at
361
- the top. `state` gives you `value` / `selected` / `disabled` for the row. Give your
362
- root `flex: 1; min-width: 0` so it fills the row (an inner `margin-inline-start: auto`
363
- right-aligns a trailing element, like the status `Tag` here).
364
-
365
- `filter` still matches the option's `value`, `label`, and `hint`. Select does
366
- not highlight matches in custom content because it cannot identify text within
367
- the returned node. `indicator` and `selection` still provide the selection mark.
368
- Use `renderIndicator` to replace that mark too; see
330
+ Use `renderOption` when people need richer context to choose an option, such
331
+ as a run's status and time. Attach that information to each option and render
332
+ it beside the label. Keep `label` meaningful because it becomes the closed
333
+ field's text and remains searchable.
334
+
335
+ Select provides the selectable row, keyboard behavior, accessible attributes,
336
+ and selection indicator. Your renderer supplies the content. Give its root
337
+ `flex: 1` and `min-width: 0` so it can fill and shrink within the row.
338
+
339
+ Search still uses the option's value, label, and hint. Custom content is not
340
+ highlighted automatically. To replace the selection mark as well, use
369
341
  [Custom indicators](#custom-indicators).
370
342
 
371
- ### Limiting the width
343
+ <a id="limiting-the-width"></a>
344
+
345
+ ### Menu width and long labels
372
346
 
373
347
  ```tsx
374
348
  // Same ellipsis-ready row for both; it truncates once the menu is capped.
@@ -391,19 +365,16 @@ const renderPath = (o) => (
391
365
  </div>
392
366
  ```
393
367
 
394
- A menu **grows to fit its widest row**. The trigger width sets the floor; the
395
- viewport sets the ceiling. Built-in `label`s stay bounded (they wrap), but **custom
396
- `renderOption` content does whatever your CSS says**, so wide content stretches the
397
- whole menu. Open both dropdowns: same rows, yet the first grows past its 220px field
398
- while the second (capped) stays put and ellipsizes.
368
+ Cap the menu width when long option content should stay within a compact
369
+ layout. The examples show the same paths: one menu grows to fit them, while the
370
+ other stays within the field's width and truncates the labels.
371
+
372
+ Set `max-width` on the menu's `::part(menu)`. Give custom row content
373
+ `min-width: 0` and the single-line ellipsis styles shown in the recipe.
374
+ Both are needed: shrinkable text alone does not limit the menu's width.
399
375
 
400
- Enforce the limit on the **menu**: cap it with **`::part(menu) { max-width }`**. Only
401
- then does ellipsis-ready content truncate, so give the row a shrinkable box
402
- (`min-width: 0`) and single-line ellipsis (`overflow: hidden; text-overflow:
403
- ellipsis; white-space: nowrap`). Nest a **`Tooltip truncatedOnly`** in that element
404
- to reveal the full text on hover, only when it's clipped. `min-width: 0` alone won't
405
- bound it: a shrink-to-fit popover grows to its content's max-content width, so the
406
- cap is what does the work.
376
+ Use `Tooltip truncatedOnly` to reveal a full label when it has been clipped.
377
+ Built-in labels wrap; this recipe is for custom single-line content.
407
378
 
408
379
  ### Custom indicators
409
380
 
@@ -427,26 +398,33 @@ cap is what does the work.
427
398
  />
428
399
  ```
429
400
 
430
- **`renderIndicator(state)`** replaces the row's selection **mark** with your own
431
- node, drawn at the **leading** edge. The row stays the control: keep an `indicator`
432
- (`'check'` / `'radio'`) or `selection="multiple"` for the `role` + `aria-checked`
433
- semantics, and only the drawn glyph changes. `state` gives you `value` / `selected` /
434
- `disabled`, so the mark can reflect selection (here, an outline ring that fills when
435
- chosen). It composes with `renderOption`, so you can override the content, the mark,
436
- both, or neither.
401
+ Use `renderIndicator` to draw a selection mark that fits your content. This
402
+ example fills a colored ring when a log level is selected.
403
+
404
+ Keep `indicator="check"`, `indicator="radio"`, or `selection="multiple"` so
405
+ the row exposes its checked state to assistive technology. Render a decorative
406
+ mark; Select keeps the row itself selectable. You can combine this with
407
+ `renderOption` to customize both the content and its indicator.
437
408
 
438
409
  ### Selection summary
439
410
 
440
411
  ```tsx
441
- const teams = ['Engineering', 'Design', 'Operations', 'Sales', 'Support']
412
+ const teams = [
413
+ { value: 'eng', label: 'Engineering' },
414
+ { value: 'design', label: 'Design' },
415
+ { value: 'ops', label: 'Operations' },
416
+ { value: 'sales', label: 'Sales' },
417
+ { value: 'support', label: 'Support' },
418
+ ]
442
419
  const [a, setA] = useState(['eng', 'design', 'ops'])
443
420
  const [b, setB] = useState(['eng', 'design'])
444
421
 
445
422
  // verbose: the count summary lists the picks
446
- <Select selection="multiple" placeholder="Pick teams" options={teams} value={a} onValueChange={setA} verbose />
423
+ <Select label="verbose" selection="multiple" placeholder="Pick teams" options={teams} value={a} onValueChange={setA} verbose />
447
424
 
448
425
  // renderSummary: build the text yourself
449
426
  <Select
427
+ label="renderSummary"
450
428
  selection="multiple"
451
429
  placeholder="Pick teams"
452
430
  options={teams}
@@ -463,20 +441,16 @@ const [b, setB] = useState(['eng', 'design'])
463
441
  />
464
442
  ```
465
443
 
466
- **`renderSummary(selected)`** builds the multi-select field text in place of the
467
- built-in "`All` / one label / `N selected`". It runs while something is selected — an
468
- empty selection still shows the `placeholder` — and takes the resolved `selected`
469
- options (`selected.length` is the count). Return `undefined` for any case you'd rather
470
- leave to the default, as above for the single-label case.
444
+ Use `verbose` when people should see the chosen labels in a multiple-selection
445
+ field. For several choices, it shows a count followed by their names. Long text
446
+ truncates to the field's width.
471
447
 
472
- Return a **string**: it flows into the same Button label the default uses, so a
473
- long summary **ellipsizes at the field's width** the way a long value does
474
- (`Engineering, Design, … `). For rich content — chips, several nodes — reach for
475
- `renderTrigger` below, which replaces the whole field.
448
+ Use `renderSummary` for task-specific text, such as “All teams” or “2 teams”.
449
+ Return `undefined` to keep the standard summary for a case, as this example
450
+ does for a single selection. An empty selection shows the placeholder.
476
451
 
477
- For the common case of just listing the picks, skip `renderSummary` and pass
478
- **`verbose`**: the count summary becomes `3 selected: A, B, C` (labels comma-joined),
479
- ellipsizing at the field width like any long value. `renderSummary` overrides it.
452
+ The summary is text. For chips or other rich content, use a
453
+ [Custom trigger](#custom-trigger).
480
454
 
481
455
  ### Custom trigger
482
456
 
@@ -526,31 +500,30 @@ const [hidden, setHidden] = useState([])
526
500
  />
527
501
  ```
528
502
 
529
- **`renderTrigger(state)`** replaces the default field. Return exactly one
530
- focusable element, such as the Anta `Button` in this example. Select positions
531
- the menu relative to that element and opens it when it is clicked. A fragment,
532
- multiple sibling elements, or a non-focusable wrapper prevents Select from finding
533
- the trigger. It logs a console warning when that happens. Add
534
- `aria-haspopup={filter ? 'dialog' : 'menu'}` and
535
- `aria-expanded={state.open}` to the element.
503
+ Use `renderTrigger` when a button or another control suits the task better
504
+ than a field. The first example opens a field filter with a selection
505
+ count. The second configures column visibility: a selected option means that
506
+ its column is hidden, and the eye indicator reflects that choice.
536
507
 
537
- `state` includes `open`, `value`, `selected`, `disabled`, and `icon`. `selected`
538
- is the resolved option list, so `selected.length` is the multi-select count. The
539
- default field props, including `label`, `size`, and `status`, do not apply to a
540
- custom trigger. `state.icon` contains the Select `icon` prop so the replacement
541
- trigger can display the same icon.
508
+ Return one focusable element with a visible label or accessible name. Add
509
+ `aria-haspopup="menu"` and `aria-expanded={open}` as shown. For a searchable
510
+ Select, use `aria-haspopup="dialog"` instead.
542
511
 
543
- The second example uses a custom trigger and `renderIndicator` for a **column
544
- visibility** control. The options are column names. In this example, a selected
545
- option means that its column is hidden: the indicator changes from `eye` to
546
- `eye-closed`. The Tag uses `selected.length`, so it appears only after a column
547
- is hidden.
512
+ Style and label the returned element directly. Default field props such as
513
+ `label`, `size`, and `status` apply to the standard field. Use the renderer's
514
+ `disabled` state to disable your custom trigger when appropriate.
548
515
 
549
- ### Empty state
516
+ You can replace the trigger as the selection or layout changes, including
517
+ switching between a custom trigger and the default field. The menu reconnects
518
+ to the replacement element.
519
+
520
+ <a id="empty-state"></a>
521
+
522
+ ### Empty and loading states
550
523
 
551
524
  ```tsx
552
525
  const [options, setOptions] = useState(['bug', 'feature', 'chore', 'docs'])
553
- const [value, setValue] = useState()
526
+ const [value, setValue] = useState<string | null>(null)
554
527
  const [loading, setLoading] = useState(false) // application fetch state
555
528
 
556
529
  <Select
@@ -577,84 +550,42 @@ const [loading, setLoading] = useState(false) // application fetch state
577
550
  />
578
551
  ```
579
552
 
580
- **`renderEmpty({ query })`** renders content when the option list is empty after
581
- filtering. It can return a "No results" message, a loading indicator, or a row
582
- that creates an option from `query`. A non-empty `query` means that filtering
583
- removed every option; an empty `query` means that no options were supplied.
584
-
585
- Select has no built-in empty message. If `renderEmpty` is omitted, an empty list
586
- has no content. Loading also belongs to application state, so return a
587
- `'Loading…'` branch when that state is true.
588
-
589
- The returned content replaces the option rows. A `MenuSeparator` with text is a
590
- muted caption and an `aria-live="polite"` region. Return a `MenuItem`, such as
591
- the `Create "…"` row, when the content must be selectable by keyboard.
592
-
593
- ### Groups and submenus
594
-
595
- ```tsx
596
- // The same tree drives both modes — selection is global, so `selection` is the
597
- // only difference between the two selects below.
598
- const fields = [
599
- 'output_text', // a plain option
600
- { label: 'Log', options: ['stream', 'message'] }, // an inline group
601
- {
602
- label: 'Metadata', // a submenu (flyout)
603
- icon: 'braces',
604
- submenu: [
605
- 'metadata.host',
606
- { label: 'Tags', options: ['tag.env', 'tag.team'] }, // group inside submenu
607
- { label: 'Custom', submenu: ['custom.a', 'custom.b'] }, // submenu inside submenu
608
- ],
609
- },
610
- { label: 'Time', submenu: ['vtime'], disabled: true }, // disabled → cascades
611
- ]
612
-
613
- <Select label="Field · single" filter options={fields} value={one} onValueChange={setOne} />
614
-
615
- <Select label="Fields · multiple" selection="multiple" filter options={fields} value={many} onValueChange={setMany} />
616
- ```
617
-
618
- An `options` entry is a plain option, a **group** (`{ label, options }`, an inline
619
- titled section), or a **submenu** (`{ label, submenu }`, a flyout). They nest and mix
620
- freely. Discriminated by shape — an `options` array is a group, a `submenu` array is a
621
- submenu, everything else is an option (so those two keys are reserved on an option).
553
+ Use `renderEmpty` to explain an empty list or offer a next step. This example
554
+ shows loading feedback while the application fetches options, then lets people
555
+ create a tag when their search has no matches.
622
556
 
623
- **Selection stays global.** The Select's `selection` (single / multiple) applies to
624
- every leaf wherever it sits; group headings and submenu parents organize but aren't
625
- selectable. One `value`, one `onValueChange`, one count — unchanged. The two selects
626
- above share the exact same `fields` tree and differ only by `selection`. `disabled` on
627
- a group or submenu cascades to all its descendants. Because selection is global, a
628
- leaf's `value` must be **unique across the whole tree** — the same value under two
629
- sections is one pick (dev builds warn).
557
+ Select provides no default empty message. Return the message your task needs.
558
+ Loading state belongs to the application; pass an empty options list while
559
+ loading when you want this content to replace the rows.
630
560
 
631
- **A query flattens the tree.** With `filter`, typing collapses submenus into inline
632
- groups (their label becomes the heading) and drops empty ones, so results read as one
633
- scannable list instead of hiding matches behind flyouts.
561
+ Use `MenuSeparator` for a status message announced politely to assistive
562
+ technology. Use `MenuItem` for an action that people can reach and select with
563
+ the keyboard. Validate the query before offering to create an option.
634
564
 
635
565
  ## Component props
636
566
 
637
567
  | Prop | Type | Default | Description |
638
568
  |------|------|---------|-------------|
639
569
  | `options` | SelectItem[] | — | The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and submenus nest and mix with plain options. Selection stays global (one `value`, leaf options only); a filter query flattens the tree into grouped results. `Select` infers its value type `V` from these options: `{ value: 365 }` makes `onValueChange` report `number`. A mix of value types widens `V` to the union. Each leaf `value` is the option's identity and must be **unique across the whole tree** (selection is value-keyed, so a value repeated in two sections is one logical pick: both rows toggle together, the trigger resolves to the last). Values that stringify alike (`365` and `"365"`) also collide as row keys; dev builds `console.warn` on either. |
640
- | `clearable?` | boolean | — | Add a "Clear" row pinned in the menu **footer** that empties the selection (single → none, multiple → `[]`). Shown only while something is selected, so it never scrolls away in a long or filtered list. |
570
+ | `clearable?` | boolean | — | Add a "Clear" row pinned in the menu **footer** that empties the selection (single → `null`, multiple → `[]`). Shown only while something is selected, so it never scrolls away in a long or filtered list. |
641
571
  | `clearLabel?` | string | Clear | Label for the `clearable` footer row. |
642
- | `defaultValue?` | V \| V[] | — | Initial selected option value for uncontrolled use. |
572
+ | `defaultValue?` | V \| null \| V[] | — | Initial option value for uncontrolled use. `null` starts empty. |
573
+ | `dimActions?` | boolean | true | Dim the default trigger's leading content and chevron at rest. They brighten on hover or focus. Ignored by `renderTrigger`. |
643
574
  | `disabled?` | boolean | — | Disable the whole select. |
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). |
575
+ | `filter?` | boolean \| (option, query) => boolean | — | Add a search field that filters option values, labels, and hints. `true` uses case-insensitive substring matching. A space in the trimmed query matches a run of whitespace. Built-in row text highlights matches. A custom `(option, query) => boolean` receives the trimmed query and returns whether to keep the option, without automatic highlighting. It runs once per option on each render, including for an empty query. Rows, empty content, and filtered Select all use the same results. A query flattens groups and submenus into grouped results. Closing the popup resets the query. |
645
576
  | `hint?` | string | — | Helper text under the field. |
646
577
  | `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. |
647
578
  | `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. |
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`. |
579
+ | `label?` | string | — | Visible field label and accessible name for the default trigger. If omitted, provide `aria-label` or `aria-labelledby` that identifies the field. Leading content and the chevron are hidden from assistive technologies and do not label the trigger. |
580
+ | `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. Hidden from assistive technologies. Include any meaningful prefix in `label` or `aria-label`. Ignored by `renderTrigger`. |
650
581
  | `offset?` | number | 4 | Gap in pixels between the trigger and the options menu. |
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). |
582
+ | `onValueChange?` | (value, attrs) => void | — | Reports the new value and a `{ value, option }` snapshot after a pick. Clear reports `null` with `{ all: true, selected: false }`. Narrow on `'all' in attrs` before reading `option`. Update `value` to accept a controlled change. There is no cancelable `onStateChange`; see the Input event-model note. |
652
583
  | `placeholder?` | string | — | Text shown when nothing is selected. |
653
584
  | `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 options menu relative to the trigger. The menu auto-flips vertically and clamps horizontally when needed. |
654
585
  | `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. |
655
586
  | `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`. |
656
587
  | `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. |
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. |
588
+ | `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 becomes the default trigger's value, 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
589
  | `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. |
659
590
  | `round?` | boolean \| number \| string | — | Round the field corners — `true` for fully round, or a number / CSS length. |
660
591
  | `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. |
@@ -665,10 +596,34 @@ scannable list instead of hiding matches behind flyouts.
665
596
  | `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
597
  | `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
598
  | `toneScope?` | 'all' \| 'selected' | 'all' | Apply the default row tone in every state, or only to selected rows. An option's own `toneScope` wins. |
668
- | `value?` | V \| V[] | — | Controlled value: the selected option's `value`. Update it through `onValueChange`. Leave it undefined for uncontrolled use. |
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. |
599
+ | `value?` | V \| null \| V[] | — | Controlled option value. Use `null` for an empty selection; clearing to `null` keeps the Select controlled. Omit or pass `undefined` for uncontrolled use. Empty strings, `0`, and `false` remain option values. |
600
+ | `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 default trigger's value, so it ellipsizes at the field's width when long (`3 selected: Engineering, Des… `). `renderSummary` overrides this. |
601
+
602
+ ### Change details
603
+
604
+ `onValueChange(value, attrs)` reports the new value and a snapshot of the change:
605
+
606
+ | Change | Snapshot |
607
+ | --- | --- |
608
+ | One option | `value` and the resolved `option`; multiple selection also includes `selected`. |
609
+ | Select all or Clear | `all: true` and `selected`, with no single `option`. |
610
+
611
+ Single-selection callbacks receive `V | null`; multiple-selection callbacks
612
+ receive `V[]`. Clear reports `null` or `[]` respectively. These types are the
613
+ same whether or not `clearable` shows the Clear action.
614
+
615
+ `value={null}` keeps a single Select controlled and empty. Omit `value` or use
616
+ `undefined` for uncontrolled mode. Updating a controlled value, including a
617
+ reset to `null`, does not itself call `onValueChange`.
618
+
619
+ Check `'all' in attrs` before reading `attrs.option`, including in single mode
620
+ when `clearable` is enabled. A controlled Select accepts a change when you update
621
+ `value`; it has no cancelable `onStateChange`. See the
622
+ [Input event model](./input.md#events) for the shared approach.
623
+
624
+ <a id="the-selectitem-type"></a>
670
625
 
671
- ### The `SelectItem` type
626
+ ### Option types
672
627
 
673
628
  `options` takes a `SelectItem[]`. Each entry is one of four shapes, discriminated by
674
629
  its keys — an `options` array is a group, a `submenu` array is a submenu, and a bare
@@ -704,7 +659,7 @@ the same thing):
704
659
 
705
660
  | Field | Type | Description |
706
661
  |---|---|---|
707
- | `value` | `OptionValue` | The option's identity: `string`, `number`, or `boolean` (`Select` infers the type from your `options`, compared with `===`). What `value` / `defaultValue` name and `onValueChange` reports. Required, unique across the tree. See [Value and changes](#value-and-changes) for non-string values and rich data. |
662
+ | `value` | `OptionValue` | The option's identity: `string`, `number`, or `boolean` (`Select` infers the type from your `options`, compared with `===`). What `value` / `defaultValue` name and `onValueChange` reports. Required, unique across the tree. See [Managing the value](#managing-the-value) for non-string values and rich data. |
708
663
  | `label` | `string` | Visible label. Defaults to `value`. |
709
664
  | `hint` | `string` | Secondary line under the label. |
710
665
  | `icon` | `IconShape` | Leading icon, after the selection indicator. |
@@ -764,18 +719,20 @@ inline `SelectGroup`, and a flyout `SelectSubmenu` (whose items are themselves
764
719
 
765
720
  ## Web Component
766
721
 
767
- Use the web component directly when you are not using React or Preact and a native control does not fit.
722
+ Without JSX, compose the Input and Menu elements to build a Select. There is
723
+ no separate `a-select` element to register.
768
724
 
769
- `Select` uses a layout wrapper. Compose an `<a-button>` trigger with an
770
- `<a-menu>` of options. The menu owns opening and dismissal; the short controller
771
- reflects a picked option into the field and selected row.
725
+ Compose an `<a-input button>` trigger with an `<a-menu>` of options. The menu owns opening and dismissal; the short controller
726
+ reflects a picked option into the field and selected row. Keep the Menu directly
727
+ after its trigger. Menu associates them through an ARIA element reference,
728
+ without requiring a popup ID, and preserves an authored `aria-controls`.
772
729
 
773
730
  ```html
774
731
  <div data-anta-composition="select" style="display: grid; gap: 4px; width: 280px">
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>
732
+ <a-input button value="stream" aria-haspopup="menu" aria-expanded="false">
733
+ <span slot="label">Output</span>
734
+ <a-icon slot="trailing" shape="chevron-down" aria-hidden="true"></a-icon>
735
+ </a-input>
779
736
  <a-menu role="menu">
780
737
  <a-menu-item role="menuitem" tabindex="0" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
781
738
  <a-menu-item role="menuitem" tabindex="0" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
@@ -785,21 +742,21 @@ reflects a picked option into the field and selected row.
785
742
  import '@antadesign/anta/elements'
786
743
 
787
744
  const root = document.querySelector('[data-anta-composition="select"]')
788
- const field = root.querySelector('a-button')
745
+ const field = root.querySelector('a-input')
789
746
  const menu = root.querySelector('a-menu')
790
747
  menu.addEventListener('statechange', (event) => {
791
748
  field.setAttribute('aria-expanded', String(event.detail.next === 'open'))
792
749
  })
793
750
 
794
- menu.addEventListener('click', (event) => {
751
+ menu.addEventListener('menuselect', (event) => {
795
752
  const item = event.target instanceof Element ? event.target.closest('a-menu-item') : null
796
753
  if (!item || item.closest('a-menu') !== menu) return
797
754
  menu.querySelectorAll(':scope > a-menu-item').forEach((option) => {
798
755
  const selected = option === item
799
756
  option.toggleAttribute('selected', selected)
800
757
  })
801
- field.querySelector('a-button-label').textContent = item.querySelector('a-menu-item-label').textContent.trim()
802
- })
758
+ field.value = item.querySelector('a-menu-item-label').textContent.trim()
759
+ }, true)
803
760
  </script>
804
761
  ```
805
762
 
@@ -808,8 +765,8 @@ reflects a picked option into the field and selected row.
808
765
  When a native single-value select is enough, add `data-anta` to `<select>`. The
809
766
  modern customizable-select model keeps native selection, keyboard behavior, and
810
767
  form submission while exposing its trigger `button`, `selectedcontent`, options,
811
- and optgroups for styling. Anta styles every supported part; browsers without that
812
- model retain their regular native select. Use `Select` for filtering, multiple
768
+ and optgroups for styling. Anta styles the customizable parts. Browser support and fallback behavior are
769
+ covered in [Install and configure](../install-config.md#browser-support). Use `Select` for filtering, multiple
813
770
  selection, nested menus, or custom option coordination.
814
771
 
815
772
  Use `data-anta-size="small"` or `"large"` for the field size. Native selects
@@ -836,7 +793,7 @@ corners.
836
793
 
837
794
  ## Styling
838
795
 
839
- Select composes an [Anta Button](./button.md) and a [Menu](./menu.md). Reach for props first, then plain CSS or `::part` for
796
+ Select composes an [Input](./input.md) and a [Menu](./menu.md). Reach for props first, then plain CSS or `::part` for
840
797
  the rest. Don't override an element's internal `--*` output tokens.
841
798
 
842
799
  **Selection color** routes through `tone`: set it per option or on the Select as a
@@ -844,28 +801,75 @@ default, then use `toneScope="selected"` for chosen rows only. It takes a named
844
801
  its hue, with lightness pinned to the brand text).
845
802
 
846
803
  **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
804
+ `className`, forwarded to `<a-input>`. Style the field through `::part(field)`
805
+ and its selected text through `::part(input-label)`. `::part(input)` targets
806
+ the native button for alignment and other control styles. The trigger uses
849
807
  `:focus-visible` for its focus ring and ellipsizes long values. **The popover**
850
808
  takes Menu's `::part(menu)`.
851
809
 
810
+ The leading content and chevron dim at rest and brighten on hover or focus.
811
+ Set `dimActions={false}` to keep them fully visible. Custom triggers control
812
+ their own adornments. Use Input's parts to style slots and `a-select-chevron`
813
+ to style the chevron.
814
+
815
+ Select's leading layout, chevron color, and custom indicator wrapper use external
816
+ CSS. Consumer `style` props, custom color or radius values, and render-hook content
817
+ can still produce inline styles. Follow the [CSP guidance](../install-config.md#content-security-policy)
818
+ when choosing styles for a restricted policy.
819
+
820
+ ### Migrate field styles
821
+
822
+ The default Select field now renders `<a-input button>` and uses Input's label,
823
+ hint, and leading slots. This changes the wrapper's DOM structure. Pass a class
824
+ name through `className` to target your Select in CSS. The class is applied to
825
+ `<a-input>`, and `::part(field)` selects the visible field inside its shadow DOM.
826
+ For example, this Select rounds its field and gives the selected text more weight:
827
+
828
+ ```tsx
829
+ <Select className="my-select" label="Team" options={['Design', 'Engineering']} />
830
+ ```
831
+
832
+ ```css
833
+ .my-select::part(field) {
834
+ border-radius: 6px;
835
+ }
836
+ .my-select::part(input-label) {
837
+ font-weight: 600;
838
+ }
839
+ ```
840
+
841
+ With `className="my-select"`, update existing selectors as follows:
842
+
843
+ | Previous selector | Input field selector |
844
+ | --- | --- |
845
+ | `.my-select > a-button` | `.my-select::part(field)` |
846
+ | `.my-select > a-button > a-button-label` | `.my-select::part(input-label)` |
847
+ | `.my-select > a-select-label` | `.my-select::part(label)` |
848
+ | `.my-select > a-select-hint` | `.my-select::part(hint)` |
849
+ | `.my-select a-select-leading` | `.my-select::part(leading)` |
850
+
851
+ Existing raw compositions using `a-select-field`, `a-select-label`,
852
+ `a-select-hint`, and `a-select-leading` retain their CSS, JSX types, and Button
853
+ label and hint behavior. New raw compositions can use `<a-input button>` as
854
+ shown above.
855
+
852
856
  ```tsx
853
857
  // Tone the selection; give the trigger a width.
854
858
  <Select tone="brand" toneScope="selected" style={{ width: '220px' }} options={OPTIONS} />
855
859
  ```
856
860
 
857
- **Borderless trigger.** The field's border is an inset `box-shadow`, so you can drop
858
- it *at rest* and let the standard hover / focus shadow return on its own; no need to
859
- re-declare it. Pair that with hiding the chevron for a field that reads as plain text
860
- until you reach for it. Hide the chevron by targeting `a-select-chevron`.
861
+ **Borderless trigger.** Hide the field's inset `box-shadow` while it is not hovered.
862
+ The standard hover border and `:focus-visible` keyboard focus outline remain
863
+ available. Hide the chevron by targeting `a-select-chevron` to make the field
864
+ read as plain text at rest.
861
865
 
862
866
  ```css
863
- /* Drop the resting border-shadow; the standard hover / focus shadow still shows. */
864
- .ghost-select a-select-field > a-button:not(:hover):not(:focus-visible) {
867
+ /* Hide the border off hover; keep the standard keyboard focus outline. */
868
+ .ghost-select a-input:not(:hover)::part(field) {
865
869
  box-shadow: none;
866
870
  }
867
871
  /* Center the chosen label in the field. */
868
- .ghost-select a-select-field > a-button > a-button-label { text-align: center; }
872
+ .ghost-select a-input::part(input) { text-align: center; }
869
873
  /* Hide the chevron. */
870
874
  .ghost-select a-select-chevron { display: none; }
871
875
  ```