@antadesign/anta 0.3.18 → 0.3.20

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 (92) hide show
  1. package/README.md +38 -242
  2. package/dist/anta_helpers.d.ts +2 -2
  3. package/dist/components/Banner.d.ts +3 -3
  4. package/dist/components/Button.d.ts +1 -1
  5. package/dist/components/Calendar.d.ts +11 -12
  6. package/dist/components/Card.d.ts +1 -1
  7. package/dist/components/Card.js +1 -1
  8. package/dist/components/Checkbox.d.ts +8 -9
  9. package/dist/components/Dialog.d.ts +1 -1
  10. package/dist/components/Expander.d.ts +5 -6
  11. package/dist/components/Input.d.ts +6 -3
  12. package/dist/components/Input.js +3 -1
  13. package/dist/components/InputAutocomplete.d.ts +8 -8
  14. package/dist/components/InputDate.css +1 -0
  15. package/dist/components/InputDate.d.ts +1 -0
  16. package/dist/components/InputDate.js +5 -5
  17. package/dist/components/InputTime.d.ts +3 -3
  18. package/dist/components/InputTime.js +1 -1
  19. package/dist/components/Menu.d.ts +7 -3
  20. package/dist/components/Menu.js +3 -2
  21. package/dist/components/MenuItem.d.ts +1 -1
  22. package/dist/components/MenuItem.js +1 -1
  23. package/dist/components/Progress.d.ts +1 -1
  24. package/dist/components/RadioGroup.d.ts +12 -13
  25. package/dist/components/Select.d.ts +32 -38
  26. package/dist/components/Select.js +9 -13
  27. package/dist/components/SelectFaceted.d.ts +28 -33
  28. package/dist/components/SelectFaceted.js +29 -23
  29. package/dist/components/Slider.d.ts +5 -5
  30. package/dist/components/Switch.d.ts +4 -4
  31. package/dist/components/Tabs.d.ts +7 -7
  32. package/dist/components/Title.d.ts +1 -1
  33. package/dist/components/Toaster.d.ts +1 -1
  34. package/dist/components/Tooltip.d.ts +1 -1
  35. package/dist/components/select-parts.css +1 -0
  36. package/dist/elements/a-card.css +1 -1
  37. package/dist/elements/a-dialog.css +1 -1
  38. package/dist/elements/a-expander.css +1 -1
  39. package/dist/elements/a-expander.d.ts +1 -0
  40. package/dist/elements/a-expander.js +12 -2
  41. package/dist/elements/a-input.css +1 -1
  42. package/dist/elements/a-menu.js +12 -0
  43. package/dist/elements/a-progress.css +1 -1
  44. package/dist/elements/a-slider.css +1 -1
  45. package/dist/elements/a-tabs.css +1 -1
  46. package/dist/elements/a-tabs.d.ts +3 -0
  47. package/dist/elements/a-tabs.js +40 -1
  48. package/dist/elements/a-tooltip.css +1 -1
  49. package/dist/elements/a-tooltip.d.ts +1 -1
  50. package/dist/elements/a-tooltip.js +1 -1
  51. package/dist/elements/index.d.ts +1 -1
  52. package/dist/general_types.d.ts +24 -22
  53. package/dist/index.d.ts +3 -3
  54. package/dist/jsx-runtime.d.ts +9 -0
  55. package/docs/accessibility.md +34 -0
  56. package/docs/changelog.md +561 -0
  57. package/docs/colors.md +42 -0
  58. package/docs/comparison.md +28 -0
  59. package/docs/components/banner.md +267 -0
  60. package/docs/components/button.md +603 -0
  61. package/docs/components/card.md +321 -0
  62. package/docs/components/checkbox.md +296 -0
  63. package/docs/components/dialog.md +314 -0
  64. package/docs/components/expander.md +526 -0
  65. package/docs/components/icon.md +155 -0
  66. package/docs/components/input-autocomplete.md +100 -0
  67. package/docs/components/input-date.md +401 -0
  68. package/docs/components/input-time.md +174 -0
  69. package/docs/components/input.md +610 -0
  70. package/docs/components/loader.md +139 -0
  71. package/docs/components/menu.md +511 -0
  72. package/docs/components/progress.md +182 -0
  73. package/docs/components/radio.md +368 -0
  74. package/docs/components/select-faceted.md +451 -0
  75. package/docs/components/select.md +926 -0
  76. package/docs/components/slider.md +147 -0
  77. package/docs/components/switch.md +198 -0
  78. package/docs/components/tabs.md +471 -0
  79. package/docs/components/tag.md +185 -0
  80. package/docs/components/text.md +148 -0
  81. package/docs/components/title.md +146 -0
  82. package/docs/components/toaster.md +313 -0
  83. package/docs/components/tooltip.md +177 -0
  84. package/docs/credits.md +18 -0
  85. package/docs/index.md +52 -0
  86. package/docs/install-config.md +240 -0
  87. package/docs/normalization.md +102 -0
  88. package/docs/overview.md +10 -0
  89. package/docs/packages/stickers.md +187 -0
  90. package/docs/packages/table.md +153 -0
  91. package/docs/theming.md +13 -0
  92. package/package.json +10 -6
@@ -0,0 +1,926 @@
1
+ # Select
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.
6
+
7
+ Select uses an [Input](./input.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.
11
+
12
+ ## Options
13
+
14
+ ```tsx
15
+ <Select
16
+ label="Field"
17
+ options={[
18
+ { value: 'output_text', hint: 'Raw log message or event payload', icon: 'file' },
19
+ { value: 'stream', hint: 'Log level: error · info · internal', icon: 'braces' },
20
+ { value: 'container', hint: 'Host where the event occurred', icon: 'cube' },
21
+ { value: 'archived', hint: 'Read-only snapshot', icon: 'folder-close', disabled: true },
22
+ { value: 'flagged', hint: 'Needs review', icon: 'warning-triangle', tone: 'warning' },
23
+ { value: 'custom', hint: 'Your own field path', icon: 'asterisk', tone: '#c026d3' },
24
+ ]}
25
+ />
26
+ ```
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.
33
+
34
+ ## Selection modes
35
+
36
+ ```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)
41
+
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} />
45
+
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
+ />
53
+ ```
54
+
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
+ The standard tooltip describes this shortcut when the pointer is over a row.
74
+ Set `SelectOption.tooltip` to replace it, or to `''` to hide it. The shortcut
75
+ is available only when `selectAll` is enabled, so `selectAll={false}` removes it.
76
+
77
+ Each checkable row is the control itself (`role="menuitemcheckbox"` /
78
+ `menuitemradio`, `aria-checked`); the checkbox / radio / check is a passive indicator
79
+ the row drives.
80
+
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
84
+ `indicator` `none` / `check`, which show the row tint; `radio` / `checkbox` have no
85
+ row tint, so it tones the label and indicator only.
86
+
87
+ ## Filter
88
+
89
+ ```tsx
90
+ // Built-in matcher: case-insensitive substring of value / label / hint
91
+ <Select filter indicator="check" options={OPTIONS} value={v} onValueChange={setV} />
92
+
93
+ // Custom matcher: a per-option predicate
94
+ <Select
95
+ filter={(option, query) => option.value.startsWith(query)}
96
+ options={OPTIONS}
97
+ value={v}
98
+ onValueChange={setV}
99
+ />
100
+ ```
101
+
102
+ Add **`filter`** for a search field at the top of the menu that narrows the options
103
+ as you type. `true` uses the built-in matcher: a case-insensitive substring of the
104
+ option's **value, label, or hint**. It's **whitespace-flexible** (a typed space
105
+ matches any run of whitespace) and **bolds the matched substring** in the results.
106
+ Pass a **function** `(option, query) => boolean` for custom matching (no highlight).
107
+
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.
114
+
115
+ ## Value and changes
116
+
117
+ ```tsx
118
+ const [field, setField] = useState('stream')
119
+
120
+ <Select
121
+ label="Field"
122
+ options={['output_text', 'stream', 'container', 'vtime', 'custom']}
123
+ value={field}
124
+ onValueChange={(v) => setField(v)}
125
+ />
126
+ ```
127
+
128
+ The value is the **option's `value`** — `value="stream"` in single mode, an array
129
+ like `value={['stream', 'container']}` in `multiple`. It's the option's identity:
130
+ what `value` / `defaultValue` name and what `onValueChange` reports, so keep each
131
+ option's `value` **unique across the whole tree**. Selection is value-keyed and
132
+ global across groups / submenus, so a value repeated in two sections is one logical
133
+ pick (both rows toggle together; the trigger resolves to the last). Dev builds
134
+ `console.warn` on a duplicate.
135
+
136
+ Values aren't limited to strings. `Select` **infers its value type from `options`**:
137
+ pass `options={[{ value: 365 }, { value: 90 }]}` and `value`, `onValueChange`'s value,
138
+ and `attrs.value` are all `number` (the same for `boolean`). Values compare with `===`,
139
+ so `365` and `"365"` stay distinct and no string conversion is needed. The exported
140
+ `OptionValue` type is `string | number | boolean`; for anything richer (a date, an
141
+ object), give `value` a **stable primitive key** (a date's ISO string, a record's id)
142
+ and read the full value back off `attrs.option`.
143
+
144
+ ```tsx
145
+ // Use the ID as the value and attach the full record to the option.
146
+ <Select
147
+ options={runs.map((r) => ({ value: r.id, label: r.name, run: r }))}
148
+ onValueChange={(id, attrs) => { if (!('all' in attrs)) openRun(attrs.option.run) }}
149
+ />
150
+
151
+ // Use an ISO string as the value, then convert it back to a Date in the callback.
152
+ <Select
153
+ options={days.map((d) => ({ value: d.toISOString(), label: fmt(d) }))}
154
+ onValueChange={(iso) => setDay(new Date(iso))}
155
+ />
156
+ ```
157
+
158
+ For an **uncontrolled** Select, omit `value` and pass `defaultValue`. For a
159
+ **controlled** Select, pass `value` and update it in `onValueChange`. The field
160
+ then shows the value supplied by the application.
161
+
162
+ **`onValueChange(value, attrs)`** is the single selection callback. The new value
163
+ comes first (a single value, or an array in `multiple` mode), with a snapshot
164
+ second: `{ value, option }` for the changed row, plus `selected` in `multiple` mode
165
+ (or `{ all: true }` for the Select-all row). There's **no `onStateChange`**: Select
166
+ has no discrete element state to veto (Checkbox and RadioGroup do), so it follows
167
+ Input's model of one value callback, and a controlled consumer rejects a pick by not
168
+ updating `value`. See the [event-model note on Input](./input.md#events) for
169
+ the rationale.
170
+
171
+ ### Deriving the selection tree
172
+
173
+ The callback hands you the new value (and the one row that changed); to read the
174
+ whole selection back against your hierarchy, use **`optionsWithSelection(options,
175
+ values)`**. It returns your `options` tree with every leaf marked `selected` and
176
+ every group / submenu carrying a rolled-up `selectionState` — `'all'`, `'none'`, or
177
+ `'some'` of its descendants. It's a pure function (no `Select` instance, reads
178
+ nothing off the event), so it behaves the same controlled or uncontrolled: hand it
179
+ your current `value` and render a grouped summary, a section indicator, or a diff.
180
+
181
+ The panel on the right is that projection, re-derived on every pick. Toggle scopes and
182
+ watch the leaf checks and the section `Tag`s (`some` / `all`) update:
183
+
184
+ ```tsx
185
+ import { Select, optionsWithSelection } from '@antadesign/anta'
186
+
187
+ const [values, setValues] = useState<string[]>(['repo.read', 'billing.view'])
188
+
189
+ // `values` drives the Select; the tree is derived from it and rendered read-only.
190
+ const tree = optionsWithSelection(scopes, values)
191
+ // tree: [ { label: 'Repositories', selectionState: 'some', options: [
192
+ // { value: 'repo.read', label: 'Read', selected: true },
193
+ // { value: 'repo.write', label: 'Write', selected: false }, … ] },
194
+ // { label: 'Members', selectionState: 'some', options: [ …,
195
+ // { label: 'Billing', selectionState: 'all', submenu: [
196
+ // { value: 'billing.view', label: 'View invoices', selected: true },
197
+ // { value: 'billing.manage', label: 'Manage plan', selected: true } ] } ] },
198
+ // { value: 'audit.read', label: 'Audit log', selected: false } ]
199
+
200
+ <Select selection="multiple" options={scopes} value={values} onValueChange={setValues} />
201
+ {/* render `tree` however you like — a summary, section indicators, a diff */}
202
+ ```
203
+
204
+ **Don't feed the tree back into `options` to control selection.** The projection
205
+ flows one way: `value` controls the `Select`, and the tree is a derived, read-only
206
+ view of it. `Select` resolves selection purely from `value` and never reads a
207
+ `selected` flag off `options`, so a `selected: true` in the tree changes nothing — it
208
+ rides along as inert data. Control the `Select` with the value array; keep the
209
+ annotated tree for rendering.
210
+
211
+ ```tsx
212
+ // ✗ Wrong — `selected` is ignored, so the Select isn't controlled by this
213
+ <Select options={optionsWithSelection(scopes, values)} />
214
+
215
+ // ✓ Right — selection is value-keyed; drive it with `value`
216
+ <Select options={scopes} value={values} onValueChange={setValues} />
217
+ ```
218
+
219
+ Bare-string options normalize to `{ value, label }`; a value under more than one
220
+ section marks the leaf everywhere it occurs (selection is value-keyed).
221
+
222
+ ## Size and status
223
+
224
+ ```tsx
225
+ <Select label="Small · info" size="small" status="info" hint="Lowercase & dashes only" options={OPTIONS} defaultValue="stream" />
226
+ <Select label="Medium · success" size="medium" status="success" hint="Field is valid" options={OPTIONS} defaultValue="stream" />
227
+ <Select label="Large · critical" size="large" status="critical" hint="Pick a field" options={OPTIONS} placeholder="Required…" />
228
+ {/* statusIcon overrides the per-status glyph, or `false` drops it, as on Input */}
229
+ <Select status="info" statusIcon="sparkles" hint="Custom glyph" options={OPTIONS} defaultValue="stream" />
230
+ ```
231
+
232
+ The trigger is an [Input](./input.md), so it inherits the field props.
233
+ **`size`** (`small` · `medium` · `large`) sets the height and type scale.
234
+ **`status`** paints a validation tone on the border, `hint`, and chevron, and
235
+ prefixes a glyph (`info` / `success` / `warning` / `critical`); **`statusIcon`**
236
+ overrides that glyph (any shape, or `false` to drop it), as on Input. The message
237
+ rides in `hint`.
238
+
239
+ `status` is a validation *state* over the same palette as `tone`: it also carries
240
+ the glyph and, for `critical`, validity. See the
241
+ [Input status note](./input.md#status). The field also takes `label`,
242
+ `placeholder`, `round`, and `disabled`, and fills its container; set a width via
243
+ `style` / `className` (both forwarded to the field).
244
+
245
+ ## Customization
246
+
247
+ 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
249
+ `renderOption`, replace its selection mark with `renderIndicator`, or build the
250
+ same interaction from the raw elements.
251
+
252
+ Pass `placement` to control where the options menu opens relative to its field. It
253
+ uses the same values as `Menu` and still flips or clamps when space is limited.
254
+ `offset` sets the gap in pixels between the field and the menu:
255
+
256
+ ```tsx
257
+ <Select placement="top-end" offset={8} options={OPTIONS} label="Field" />
258
+ ```
259
+
260
+ ### Build it from elements
261
+
262
+ The wrapper renders a read-only `<Input>` and a `<Menu>`. It updates the field
263
+ and each option's `selected` state from the value, and reports a pick through
264
+ `onValueChange`. Menu handles positioning, dismissal, and keyboard navigation.
265
+
266
+ 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.
268
+
269
+ ```html
270
+ <!-- `.select` scopes this example's CSS. Use an application selector instead. -->
271
+ <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>
278
+ <a-menu role="menu">
279
+ <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
+ <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>
281
+ <a-menu-item role="menuitemradio" tabindex="0" aria-checked="false" value="container"><a-menu-item-label>container</a-menu-item-label></a-menu-item>
282
+ </a-menu>
283
+ </div>
284
+ <script type="module">
285
+ import '@antadesign/anta/elements'
286
+ const root = document.querySelector('.select')
287
+ const field = root.querySelector('a-input')
288
+ const menu = root.querySelector('a-menu')
289
+ const chevron = field.querySelector('a-icon')
290
+ // Flip the chevron with the menu's open state (as <Select> does).
291
+ menu.addEventListener('statechange', (e) => {
292
+ chevron.style.transform = e.detail.next === 'open' ? 'rotate(180deg)' : ''
293
+ })
294
+ // On pick: mark the row selected + reflect its label into the field.
295
+ menu.addEventListener('click', (e) => {
296
+ const item = e.target.closest('a-menu-item')
297
+ if (!item) return
298
+ menu.querySelectorAll('a-menu-item').forEach((el) => {
299
+ const selected = el === item
300
+ el.toggleAttribute('selected', selected)
301
+ el.setAttribute('aria-checked', String(selected))
302
+ })
303
+ field.value = item.querySelector('a-menu-item-label').textContent
304
+ })
305
+ </script>
306
+ ```
307
+
308
+ The React and Preact `Select` wrapper performs those updates for you.
309
+
310
+ ### Custom option rendering
311
+
312
+ ```tsx
313
+ const TONE = { in_progress: 'info', completed: 'neutral', incomplete: 'critical' }
314
+ const LABEL = { in_progress: 'In progress', completed: 'Completed', incomplete: 'Incomplete' }
315
+
316
+ // The field fills its container, so the wrapper's width bounds the trigger.
317
+ // (<Select style={{ width: 260 }} …> works too; style forwards to the field.)
318
+ <div style={{ width: 260 }}>
319
+ <Select
320
+ label="Test run"
321
+ filter // name / date stay searchable
322
+ options={runs} // { value, label, name, ranAt, status }[]
323
+ value={run}
324
+ onValueChange={setRun}
325
+ renderOption={(option) => {
326
+ const run = option as Run // index-signature fields are `unknown`
327
+ return (
328
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, gap: 2 }}>
329
+ {/* The ellipsized name is the anchor; the nested Tooltip (its children are the
330
+ bubble) reveals the full name when the name is clipped. */}
331
+ <span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
332
+ {run.name}
333
+ <Tooltip truncatedOnly>{run.name}</Tooltip>
334
+ </span>
335
+ <div style={{ display: 'flex', alignItems: 'center' }}>
336
+ <span style={{ color: 'var(--text-3)', fontSize: 12, fontVariantNumeric: 'tabular-nums' }}>{run.ranAt}</span>
337
+ <Tag size="small" tone={TONE[run.status]} style={{ marginInlineStart: 'auto' }}>{LABEL[run.status]}</Tag>
338
+ </div>
339
+ </div>
340
+ )
341
+ }}
342
+ />
343
+ </div>
344
+ ```
345
+
346
+ **`renderOption(option, state)`** replaces the built-in `label`, `hint`, and
347
+ `icon` layout for a row. Select still renders the row container, click behavior,
348
+ ARIA attributes, and selection indicator. Set each option's `label` for the
349
+ closed field text.
350
+
351
+ Attach your own fields to each option (`SelectOption` carries an index signature) and
352
+ read them back off `option`; those extra fields are typed `unknown`, so cast once at
353
+ the top. `state` gives you `value` / `selected` / `disabled` for the row. Give your
354
+ root `flex: 1; min-width: 0` so it fills the row (an inner `margin-inline-start: auto`
355
+ right-aligns a trailing element, like the status `Tag` here).
356
+
357
+ `filter` still matches the option's `value`, `label`, and `hint`. Select does
358
+ not highlight matches in custom content because it cannot identify text within
359
+ the returned node. `indicator` and `selection` still provide the selection mark.
360
+ Use `renderIndicator` to replace that mark too; see
361
+ [Custom indicators](#custom-indicators).
362
+
363
+ ### Limiting the width
364
+
365
+ ```tsx
366
+ // Same ellipsis-ready row for both; it truncates once the menu is capped.
367
+ const renderPath = (o) => (
368
+ <span style={{ display: 'block', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
369
+ {o.label}
370
+ <Tooltip truncatedOnly>{o.label}</Tooltip> {/* reveals the full path when clipped */}
371
+ </span>
372
+ )
373
+
374
+ // First: no cap, so the widest row stretches the menu past the trigger.
375
+ <div style={{ width: 220 }}>
376
+ <Select label="No cap · grows to fit" options={paths} value={a} onValueChange={setA} renderOption={renderPath} />
377
+ </div>
378
+
379
+ // Second: cap the popover so the same rows truncate to the trigger width.
380
+ <style>{`.path-capped a-menu::part(menu) { max-width: 220px }`}</style>
381
+ <div className="path-capped" style={{ width: 220 }}>
382
+ <Select label="Capped · ellipsizes" options={paths} value={b} onValueChange={setB} renderOption={renderPath} />
383
+ </div>
384
+ ```
385
+
386
+ A menu **grows to fit its widest row**. The trigger width sets the floor; the
387
+ viewport sets the ceiling. Built-in `label`s stay bounded (they wrap), but **custom
388
+ `renderOption` content does whatever your CSS says**, so wide content stretches the
389
+ whole menu. Open both dropdowns: same rows, yet the first grows past its 220px field
390
+ while the second (capped) stays put and ellipsizes.
391
+
392
+ Enforce the limit on the **menu**: cap it with **`::part(menu) { max-width }`**. Only
393
+ then does ellipsis-ready content truncate, so give the row a shrinkable box
394
+ (`min-width: 0`) and single-line ellipsis (`overflow: hidden; text-overflow:
395
+ ellipsis; white-space: nowrap`). Nest a **`Tooltip truncatedOnly`** in that element
396
+ to reveal the full text on hover, only when it's clipped. `min-width: 0` alone won't
397
+ bound it: a shrink-to-fit popover grows to its content's max-content width, so the
398
+ cap is what does the work.
399
+
400
+ ### Custom indicators
401
+
402
+ ```tsx
403
+ <Select
404
+ label="Log level"
405
+ indicator="check" // keeps the semantics (role + aria-checked)
406
+ options={levels} // { value, label, dot }[]
407
+ value={level}
408
+ onValueChange={setLevel}
409
+ renderIndicator={(state) => {
410
+ const dot = levels.find((l) => l.value === state.value).dot
411
+ return (
412
+ <span style={{
413
+ width: 10, height: 10, borderRadius: '50%', display: 'inline-block',
414
+ boxShadow: `inset 0 0 0 2px ${dot}`,
415
+ background: state.selected ? dot : 'transparent',
416
+ }} />
417
+ )
418
+ }}
419
+ />
420
+ ```
421
+
422
+ **`renderIndicator(state)`** replaces the row's selection **mark** with your own
423
+ node, drawn at the **leading** edge. The row stays the control: keep an `indicator`
424
+ (`'check'` / `'radio'`) or `selection="multiple"` for the `role` + `aria-checked`
425
+ semantics, and only the drawn glyph changes. `state` gives you `value` / `selected` /
426
+ `disabled`, so the mark can reflect selection (here, an outline ring that fills when
427
+ chosen). It composes with `renderOption`, so you can override the content, the mark,
428
+ both, or neither.
429
+
430
+ ### Selection summary
431
+
432
+ ```tsx
433
+ const teams = ['Engineering', 'Design', 'Operations', 'Sales', 'Support']
434
+ const [a, setA] = useState(['eng', 'design', 'ops'])
435
+ const [b, setB] = useState(['eng', 'design'])
436
+
437
+ // verbose: the count summary lists the picks
438
+ <Select selection="multiple" placeholder="Pick teams" options={teams} value={a} onValueChange={setA} verbose />
439
+
440
+ // renderSummary: build the text yourself
441
+ <Select
442
+ selection="multiple"
443
+ placeholder="Pick teams"
444
+ options={teams}
445
+ value={b}
446
+ onValueChange={setB}
447
+ // one label; a count past that; every option → "All teams"
448
+ renderSummary={(selected) =>
449
+ selected.length === teams.length
450
+ ? 'All teams'
451
+ : selected.length > 1
452
+ ? `${selected.length} teams`
453
+ : undefined // fall back to the single label
454
+ }
455
+ />
456
+ ```
457
+
458
+ **`renderSummary(selected)`** builds the multi-select field text in place of the
459
+ built-in "`All` / one label / `N selected`". It runs while something is selected — an
460
+ empty selection still shows the `placeholder` — and takes the resolved `selected`
461
+ options (`selected.length` is the count). Return `undefined` for any case you'd rather
462
+ leave to the default, as above for the single-label case.
463
+
464
+ Return a **string**: it flows into the same read-only field the default uses, so a
465
+ long summary **ellipsizes at the field's width** the way a long value does
466
+ (`Engineering, Design, … `). For rich content — chips, several nodes — reach for
467
+ `renderTrigger` below, which replaces the whole field.
468
+
469
+ For the common case of just listing the picks, skip `renderSummary` and pass
470
+ **`verbose`**: the count summary becomes `3 selected: A, B, C` (labels comma-joined),
471
+ ellipsizing at the field width like any long value. `renderSummary` overrides it.
472
+
473
+ ### Custom trigger
474
+
475
+ ```tsx
476
+ const [value, setValue] = useState(['stream', 'message'])
477
+
478
+ <Select
479
+ selection="multiple"
480
+ options={fields}
481
+ value={value}
482
+ onValueChange={setValue}
483
+ renderTrigger={({ open, selected }) => (
484
+ // Return one focusable element. It supplies the required ARIA attributes.
485
+ <Button icon="filter" label="Filter" priority="secondary"
486
+ aria-haspopup="menu" aria-expanded={open ? 'true' : 'false'}>
487
+ {selected.length > 0 && (
488
+ <Tag size="small" priority="primary" tone="brand">
489
+ {selected.length === fields.length ? 'All' : selected.length}
490
+ </Tag>
491
+ )}
492
+ </Button>
493
+ )}
494
+ />
495
+ ```
496
+
497
+ ```tsx
498
+ const columns = ['Name', 'Status', 'Owner', 'Created', 'Duration', 'Environment', 'Branch', 'Commit']
499
+ const [hidden, setHidden] = useState([])
500
+
501
+ <Select
502
+ selection="multiple"
503
+ options={columns}
504
+ value={hidden}
505
+ onValueChange={setHidden}
506
+ // selecting a column hides it: eye by default, eye-closed once hidden
507
+ renderIndicator={({ selected }) => (
508
+ <Icon shape={selected ? 'eye-closed' : 'eye'} />
509
+ )}
510
+ renderTrigger={({ open, selected }) => (
511
+ <Button icon="columns-3-cog" priority="tertiary" aria-label="Configure columns"
512
+ aria-haspopup="menu" aria-expanded={open ? 'true' : 'false'}>
513
+ {selected.length > 0 && (
514
+ <Tag size="small" priority="secondary" tone="neutral">{selected.length}</Tag>
515
+ )}
516
+ </Button>
517
+ )}
518
+ />
519
+ ```
520
+
521
+ **`renderTrigger(state)`** replaces the default field. Return exactly one
522
+ focusable element, such as the Anta `Button` in this example. Select positions
523
+ the menu relative to that element and opens it when it is clicked. A fragment,
524
+ multiple sibling elements, or a non-focusable wrapper prevents Select from finding
525
+ the trigger. It logs a console warning when that happens. Add
526
+ `aria-haspopup="menu"` and `aria-expanded={state.open}` to the element.
527
+
528
+ `state` includes `open`, `value`, `selected`, `disabled`, and `icon`. `selected`
529
+ is the resolved option list, so `selected.length` is the multi-select count. The
530
+ default field props, including `label`, `size`, and `status`, do not apply to a
531
+ custom trigger. `state.icon` contains the Select `icon` prop so the replacement
532
+ trigger can display the same icon.
533
+
534
+ The second example uses a custom trigger and `renderIndicator` for a **column
535
+ visibility** control. The options are column names. In this example, a selected
536
+ option means that its column is hidden: the indicator changes from `eye` to
537
+ `eye-closed`. The Tag uses `selected.length`, so it appears only after a column
538
+ is hidden.
539
+
540
+ ### Empty state
541
+
542
+ ```tsx
543
+ const [options, setOptions] = useState(['bug', 'feature', 'chore', 'docs'])
544
+ const [value, setValue] = useState()
545
+ const [loading, setLoading] = useState(false) // application fetch state
546
+
547
+ <Select
548
+ label="Tag"
549
+ filter
550
+ placeholder="Filter or create…"
551
+ options={loading ? [] : options}
552
+ value={value}
553
+ onValueChange={setValue}
554
+ renderEmpty={({ query }) =>
555
+ loading ? (
556
+ <MenuSeparator>Loading…</MenuSeparator>
557
+ ) : (
558
+ <>
559
+ <MenuSeparator>No options are matching the filter</MenuSeparator>
560
+ <MenuItem
561
+ icon="plus"
562
+ label={`Create "${query}"`}
563
+ onSelect={() => { setOptions((o) => [...o, query]); setValue(query) }}
564
+ />
565
+ </>
566
+ )
567
+ }
568
+ />
569
+ ```
570
+
571
+ **`renderEmpty({ query })`** renders content when the option list is empty after
572
+ filtering. It can return a "No results" message, a loading indicator, or a row
573
+ that creates an option from `query`. A non-empty `query` means that filtering
574
+ removed every option; an empty `query` means that no options were supplied.
575
+
576
+ Select has no built-in empty message. If `renderEmpty` is omitted, an empty list
577
+ has no content. Loading also belongs to application state, so return a
578
+ `'Loading…'` branch when that state is true.
579
+
580
+ The returned content replaces the option rows. A `MenuSeparator` with text is a
581
+ muted caption and an `aria-live="polite"` region. Return a `MenuItem`, such as
582
+ the `Create "…"` row, when the content must be selectable by keyboard.
583
+
584
+ ### Groups and submenus
585
+
586
+ ```tsx
587
+ // The same tree drives both modes — selection is global, so `selection` is the
588
+ // only difference between the two selects below.
589
+ const fields = [
590
+ 'output_text', // a plain option
591
+ { label: 'Log', options: ['stream', 'message'] }, // an inline group
592
+ {
593
+ label: 'Metadata', // a submenu (flyout)
594
+ icon: 'braces',
595
+ submenu: [
596
+ 'metadata.host',
597
+ { label: 'Tags', options: ['tag.env', 'tag.team'] }, // group inside submenu
598
+ { label: 'Custom', submenu: ['custom.a', 'custom.b'] }, // submenu inside submenu
599
+ ],
600
+ },
601
+ { label: 'Time', submenu: ['vtime'], disabled: true }, // disabled → cascades
602
+ ]
603
+
604
+ <Select label="Field · single" filter options={fields} value={one} onValueChange={setOne} />
605
+
606
+ <Select label="Fields · multiple" selection="multiple" filter options={fields} value={many} onValueChange={setMany} />
607
+ ```
608
+
609
+ An `options` entry is a plain option, a **group** (`{ label, options }`, an inline
610
+ titled section), or a **submenu** (`{ label, submenu }`, a flyout). They nest and mix
611
+ freely. Discriminated by shape — an `options` array is a group, a `submenu` array is a
612
+ submenu, everything else is an option (so those two keys are reserved on an option).
613
+
614
+ **Selection stays global.** The Select's `selection` (single / multiple) applies to
615
+ every leaf wherever it sits; group headings and submenu parents organize but aren't
616
+ selectable. One `value`, one `onValueChange`, one count — unchanged. The two selects
617
+ above share the exact same `fields` tree and differ only by `selection`. `disabled` on
618
+ a group or submenu cascades to all its descendants. Because selection is global, a
619
+ leaf's `value` must be **unique across the whole tree** — the same value under two
620
+ sections is one pick (dev builds warn).
621
+
622
+ **A query flattens the tree.** With `filter`, typing collapses submenus into inline
623
+ groups (their label becomes the heading) and drops empty ones, so results read as one
624
+ scannable list instead of hiding matches behind flyouts.
625
+
626
+ ### Props
627
+
628
+ | Prop | Type | Default | Description |
629
+ |------|------|---------|-------------|
630
+ | `options` | SelectItem[] | — | The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
631
+ (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
632
+ submenus nest and mix with plain options. Selection stays global (one `value`,
633
+ leaf options only); a filter query flattens the tree into grouped results.
634
+
635
+ `Select` infers its value type `V` from these options: `{ value: 365 }` makes
636
+ `onValueChange` report `number`. A mix of value types widens `V` to the union.
637
+
638
+ Each leaf `value` is the option's identity and must be **unique across the whole
639
+ tree** (selection is value-keyed, so a value repeated in two sections is one logical
640
+ pick: both rows toggle together, the trigger resolves to the last). Values that
641
+ stringify alike (`365` and `"365"`) also collide as row keys; dev builds
642
+ `console.warn` on either. |
643
+ | `clearable?` | boolean | — | Add a "Clear" row pinned in the menu **footer** that empties the selection
644
+ (single → none, multiple → `[]`). Shown only while something is selected, so
645
+ it never scrolls away in a long or filtered list. |
646
+ | `clearLabel?` | string | Clear | Label for the `clearable` footer row. |
647
+ | `defaultValue?` | V \| V[] | — | Initial selected option value for uncontrolled use. |
648
+ | `disabled?` | boolean | — | Disable the whole select. |
649
+ | `filter?` | boolean \| (option, query) => boolean | — | Add a search field at the top of the menu that filters the options as you
650
+ type. `true` uses the built-in matcher — a case-insensitive substring of the
651
+ option's **value / label / hint**. Pass a **function** `(option, query) =>
652
+ boolean` for custom matching (called per option; return `true` to keep it). |
653
+ | `hint?` | string | — | Helper text under the field (Input's `hint`). |
654
+ | `icon?` | IconShape | — | Leading icon shown at the left of the field (the default trigger's `Input`
655
+ `leading` slot). With a custom `renderTrigger`, it's passed through as
656
+ `state.icon` instead — the consumer places it. |
657
+ | `indicator?` | 'none' \| 'check' \| 'radio' | none | The per-row mark for **single**-select: `'none'` (a tint-only highlight),
658
+ `'check'` (a trailing checkmark on the selected row, keeping the tint — the
659
+ canonical Select look), or `'radio'` (a leading radio on every row).
660
+ Multi-select always uses checkboxes. |
661
+ | `label?` | string | — | Field label, above the trigger (Input's `label`). |
662
+ | `leading?` | ReactNode | — | Content for the default trigger's `leading` slot, such as a key prefix
663
+ before the value. It replaces the icon derived from `icon`. Include an
664
+ `<Icon>` in this content when both are needed. Ignored by `renderTrigger`. |
665
+ | `offset?` | number | 4 | Gap in pixels between the trigger and the options menu. |
666
+ | `onValueChange?` | (value, attrs) => void | — | Fires after the selection changes, with the new value and a
667
+ `{ value, option }` snapshot. Select has no discrete element state, so
668
+ there is no cancelable `onStateChange` (see the Input event-model note). |
669
+ | `placeholder?` | string | — | Text shown when nothing is selected. |
670
+ | `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
671
+ auto-flips vertically and clamps horizontally when needed. |
672
+ | `renderEmpty?` | (state) => ReactNode | — | Render content in the menu body when the (filtered) option list is empty —
673
+ a "no results" message, a loading indicator (gated on your own external
674
+ loading state), or a "create from the query" row. Receives an `EmptyState`
675
+ (`query`, trimmed). There is no built-in empty message: when omitted, an empty
676
+ list renders nothing. Whatever you return goes where the option rows would —
677
+ a plain node is inert; return a `MenuItem` (e.g. a "Create" row) to make it
678
+ focusable and selectable. |
679
+ | `renderIndicator?` | (state) => ReactNode | — | Replace each row's selection **mark** with your own node, drawn at the
680
+ leading edge. The row stays the control (`role` + `aria-checked` from
681
+ `indicator` / `selection`); only the drawn mark changes, so pair it with an
682
+ `indicator` (`'check'` / `'radio'`) or `selection="multiple"` for the
683
+ semantics. Composes with `renderOption`. |
684
+ | `renderOption?` | (option, state) => ReactNode | — | Replaces the built-in `label`, `hint`, and `icon` layout for each option row.
685
+ Select still supplies the row container, click handling, ARIA attributes, and
686
+ selection indicator. Read extra option fields through `SelectOption`'s index
687
+ signature. `state` contains `value`, `selected`, and `disabled`. Filtering
688
+ still matches the option's `value`, `label`, and `hint`, but Select cannot
689
+ highlight matches within the returned content. |
690
+ | `renderSummary?` | (selected) => string \| undefined | — | `multiple` only: build the trigger's selection summary text yourself,
691
+ replacing the built-in "`All` / one label / `N selected`" logic. Receives
692
+ the resolved selected options (`selected.length` is the count) and runs only
693
+ while something is selected — an empty selection still shows the
694
+ `placeholder`. Return a **string**: it flows into the default trigger's
695
+ read-only field, so a long summary ellipsizes at the field's width just
696
+ like a long value (`Engineering, Design, … `). Return `undefined` to fall
697
+ back to the default for that case (e.g. customize only the count, keeping
698
+ the single-label case built-in). For rich content (chips, multiple nodes)
699
+ use `renderTrigger`, which replaces the whole field. |
700
+ | `renderTrigger?` | (state) => ReactNode | — | Replaces the default field with a trigger returned from this function.
701
+ Receives `open`, `value`, `selected`, `disabled`, and `icon`. Return exactly
702
+ one focusable element, such as an Anta `Button`. The menu is positioned
703
+ relative to that element and opens when it is clicked. Do not return a
704
+ fragment, multiple siblings, or a non-focusable wrapper. Add
705
+ `aria-haspopup="menu"` and `aria-expanded={state.open}` to the element.
706
+ Field props (`label`, `hint`, `size`, `status`, `placeholder`, and `round`) and
707
+ `className` / `style` apply only to the default field. Add styling and
708
+ attributes to the returned element instead. |
709
+ | `round?` | boolean \| number \| string | — | Round the field corners — `true` for fully round, or a number / CSS length. |
710
+ | `selectAll?` | boolean | true | `multiple` only: shows a "Select all" row that toggles every enabled option,
711
+ or only the visible options when a filter query is active. Its checkbox is
712
+ mixed when some options are selected. It is on by default. Set it to `false`
713
+ to remove the row and the Alt/Option-click shortcut that selects only one row. |
714
+ | `selectAllLabel?` | string | Select all | Label for the `selectAll` row. |
715
+ | `selection?` | 'single' \| 'multiple' | single | Selection mode. `'single'` (the default) keeps `value` a single value and
716
+ closes the menu on pick. Switch to `'multiple'` for checkboxes + an
717
+ array value. |
718
+ | `size?` | 'small' \| 'medium' \| 'large' | medium | Field size. |
719
+ | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | neutral | Validation/feedback tone for the field (Input's `status`). |
720
+ | `statusIcon?` | (string & {}) \| false \| IconShape | — | Glyph shown before the `hint` when `status` is set (Input's `statusIcon`).
721
+ Each status has a default; pass a shape to override, or `false` to drop it. |
722
+ | `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to the **selected** row(s) — the whole row takes this tone
723
+ (label, icon, indicator, and the background tint), like passing `tone` to just
724
+ the chosen option. A named tone or a custom CSS color. Most visible with the
725
+ tint-based marks (`indicator` `'none'` / `'check'`); with `'radio'` /
726
+ `'checkbox'` it tones the label + indicator (those modes have no row tint). |
727
+ | `value?` | V \| V[] | — | Controlled value: the selected option's `value`. Update it through
728
+ `onValueChange`. Leave it undefined for uncontrolled use. |
729
+ | `verbose?` | boolean | — | `multiple` only: spell the picks out in the count summary — `3 selected:
730
+ A, B, C` (labels comma-joined) in place of the bare `3 selected`. Applies
731
+ to the multi-count case only: `All` stays `All`, a single pick stays its
732
+ own label, and an empty selection stays the `placeholder`. The list flows
733
+ into the read-only field, so it ellipsizes at the field's width when long
734
+ (`3 selected: Engineering, Des… `). `renderSummary` overrides this. |
735
+
736
+ ### The `SelectItem` type
737
+
738
+ `options` takes a `SelectItem[]`. Each entry is one of four shapes, discriminated by
739
+ its keys — an `options` array is a group, a `submenu` array is a submenu, and a bare
740
+ string or a plain object is a single option:
741
+
742
+ | Shape | Renders as |
743
+ |---|---|
744
+ | `string` | One option; shorthand for `{ value: s, label: s }`. |
745
+ | `SelectOption` | One option with a label, hint, icon, tone, tooltip, or your own fields. |
746
+ | `SelectGroup` | A titled group of items, **inline** under a heading. |
747
+ | `SelectSubmenu` | A titled branch of items behind a **flyout**. |
748
+
749
+ `value` is unique across the whole tree — selection is value-keyed and global, so a
750
+ group or submenu only organizes (its heading isn't selectable), and a `disabled`
751
+ group or submenu cascades to every descendant.
752
+
753
+ The smallest form is three options, each carrying only a `value` (a bare string is
754
+ the same thing):
755
+
756
+ ```tsx
757
+ <Select
758
+ label="Priority"
759
+ options={[
760
+ { value: 'low' },
761
+ { value: 'medium' },
762
+ { value: 'high' },
763
+ ]}
764
+ />
765
+ // equivalent: options={['low', 'medium', 'high']}
766
+ ```
767
+
768
+ **`SelectOption`** — one selectable row.
769
+
770
+ | Field | Type | Description |
771
+ |---|---|---|
772
+ | `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. |
773
+ | `label` | `string` | Visible label. Defaults to `value`. |
774
+ | `hint` | `string` | Secondary line under the label. |
775
+ | `icon` | `IconShape` | Leading icon, after the selection indicator. |
776
+ | `disabled` | `boolean` | Disable just this option. |
777
+ | `tone` | `string` | The row's color across label, icon, hint, selected tint, and indicator. A named tone (`neutral` / `brand` / `info` / `success` / `warning` / `critical`) or any CSS color. |
778
+ | `tooltip` | `React.ReactNode` | Row tooltip. In a `multiple` select with `selectAll`, a row with no `tooltip` shows the default "select only this" (Alt/⌥-click) hint; set it to override, or `''` to suppress. |
779
+ | `[key: string]` | `unknown` | Your own data — attach anything and read it back in `renderOption`. |
780
+
781
+ **`SelectGroup`** — a heading with items rendered inline beneath it.
782
+
783
+ | Field | Type | Description |
784
+ |---|---|---|
785
+ | `label` | `string` | The section heading (non-interactive). |
786
+ | `options` | `SelectItem[]` | The grouped items — options, or nested groups / submenus. |
787
+ | `disabled` | `boolean` | Disable the whole group (cascades to all descendants). |
788
+
789
+ **`SelectSubmenu`** — a parent row whose items live behind a flyout.
790
+
791
+ | Field | Type | Description |
792
+ |---|---|---|
793
+ | `label` | `string` | The parent row's label, and the group heading when a filter query flattens the tree. |
794
+ | `icon` | `IconShape` | Leading icon on the parent row. |
795
+ | `submenu` | `SelectItem[]` | The branch's items — options, or nested groups / submenus. |
796
+ | `disabled` | `boolean` | Disable the whole branch (cascades to all descendants). |
797
+
798
+ All four shapes mix in one `options` array — a bare string, a full `SelectOption`, an
799
+ inline `SelectGroup`, and a flyout `SelectSubmenu` (whose items are themselves
800
+ `SelectItem`s, so groups and submenus nest freely):
801
+
802
+ ```tsx
803
+ <Select
804
+ label="Field"
805
+ options={[
806
+ 'output_text', // string shorthand
807
+ { value: 'stream', label: 'Stream', hint: 'Log level', icon: 'braces' }, // SelectOption
808
+ { // SelectGroup — inline heading
809
+ label: 'Metadata',
810
+ options: [
811
+ { value: 'container', icon: 'cube' },
812
+ { value: 'file_path', icon: 'file' },
813
+ ],
814
+ },
815
+ { // SelectSubmenu — flyout branch
816
+ label: 'Advanced',
817
+ icon: 'folder-close',
818
+ submenu: [
819
+ 'raw_bytes',
820
+ { value: 'custom', hint: 'Your own field path', icon: 'asterisk', disabled: true },
821
+ ],
822
+ },
823
+ ]}
824
+ />
825
+ ```
826
+
827
+ Use the web component directly when you are not using React or Preact and a native control does not fit.
828
+
829
+ `Select` has no host element. Compose a read-only `<a-input>` trigger with an
830
+ `<a-menu>` of options. The menu owns opening and dismissal; the short controller
831
+ reflects a picked option into the field and selected row.
832
+
833
+ ```html
834
+ <div data-anta-composition="select" style="display: grid; gap: 4px; width: 280px">
835
+ <a-input readonly value="stream">
836
+ <span slot="label">Output</span>
837
+ <span slot="trailing"><a-icon shape="chevron-down" aria-hidden="true"></a-icon></span>
838
+ </a-input>
839
+ <a-menu role="menu">
840
+ <a-menu-item role="menuitem" tabindex="0" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
841
+ <a-menu-item role="menuitem" tabindex="0" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
842
+ </a-menu>
843
+ </div>
844
+ <script type="module">
845
+ import '@antadesign/anta/elements'
846
+
847
+ const root = document.querySelector('[data-anta-composition="select"]')
848
+ const field = root.querySelector('a-input')
849
+ const menu = root.querySelector('a-menu')
850
+
851
+ menu.addEventListener('click', (event) => {
852
+ const item = event.target instanceof Element ? event.target.closest('a-menu-item') : null
853
+ if (!item || item.closest('a-menu') !== menu) return
854
+ menu.querySelectorAll(':scope > a-menu-item').forEach((option) => {
855
+ const selected = option === item
856
+ option.toggleAttribute('selected', selected)
857
+ })
858
+ field.value = item.querySelector('a-menu-item-label').textContent.trim()
859
+ })
860
+ </script>
861
+ ```
862
+
863
+ ### Native HTML select
864
+
865
+ When a native single-value select is enough, add `data-anta` to `<select>`. The
866
+ modern customizable-select model keeps native selection, keyboard behavior, and
867
+ form submission while exposing its trigger `button`, `selectedcontent`, options,
868
+ and optgroups for styling. Anta styles every supported part; browsers without that
869
+ model retain their regular native select. Use `Select` for filtering, multiple
870
+ selection, nested menus, or custom option coordination.
871
+
872
+ Use `data-anta-size="small"` or `"large"` for the field size. Native selects
873
+ continue to use their standard `size` attribute, and `round` gives the trigger matching rounded
874
+ corners.
875
+
876
+ ```html
877
+ <select data-anta data-anta-size="small" round name="output">
878
+ <!-- The native trigger; selectedcontent mirrors the chosen option. -->
879
+ <button>
880
+ <selectedcontent></selectedcontent>
881
+ <a-icon shape="chevron-down" aria-hidden="true"></a-icon>
882
+ </button>
883
+
884
+ <option value="" selected disabled>Choose an output</option>
885
+ <option value="stream">Stream</option>
886
+ <option value="output_text">Output text</option>
887
+ <optgroup label="Metadata">
888
+ <option value="container">Container</option>
889
+ <option value="file_path">File path</option>
890
+ </optgroup>
891
+ </select>
892
+ ```
893
+
894
+ Select is an [Input](./input.md) plus a [Menu](./menu.md), so it
895
+ inherits both surfaces' hooks. Reach for props first, then plain CSS or `::part` for
896
+ the rest. Don't override an element's internal `--*` output tokens.
897
+
898
+ **Selection color** routes through the props: a per-option `tone`, or `toneSelected`
899
+ for the chosen row(s). Both take a named tone or any CSS color (a custom color keeps
900
+ its hue, with lightness pinned to the brand text).
901
+
902
+ **The field** is styled through Input's props (`size`, `status`, `round`) and its
903
+ `::part`s; set its width with `style` / `className` (forwarded to the field). **The
904
+ popover** takes Menu's `::part(menu)`.
905
+
906
+ ```tsx
907
+ // Tone the selection; give the trigger a width.
908
+ <Select toneSelected="brand" style={{ width: '220px' }} options={OPTIONS} />
909
+ ```
910
+
911
+ **Borderless trigger.** The field's border is an inset `box-shadow`, so you can drop
912
+ it *at rest* and let the standard hover / focus shadow return on its own; no need to
913
+ re-declare it. Pair that with hiding the chevron for a field that reads as plain text
914
+ until you reach for it. Hide the chevron by targeting the trailing `a-icon` (its slot
915
+ wrapper is inline-`display:contents`, so hit the icon itself).
916
+
917
+ ```css
918
+ /* Drop the resting border-shadow; the standard hover / focus shadow still shows. */
919
+ .ghost-select a-input:not(:hover):not(:focus-within)::part(field) {
920
+ box-shadow: none;
921
+ }
922
+ /* Center the chosen label in the field. */
923
+ .ghost-select a-input::part(input) { text-align: center; }
924
+ /* The chevron's slot wrapper is inline display:contents; hide the icon itself. */
925
+ .ghost-select a-input [slot="trailing"] a-icon { display: none; }
926
+ ```