@antadesign/anta 0.3.18 → 0.3.19

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 (59) hide show
  1. package/README.md +38 -242
  2. package/dist/components/Input.d.ts +4 -1
  3. package/dist/components/Input.js +2 -0
  4. package/dist/components/InputDate.css +1 -0
  5. package/dist/components/InputDate.d.ts +1 -0
  6. package/dist/components/InputDate.js +5 -5
  7. package/dist/components/Menu.d.ts +5 -1
  8. package/dist/components/Menu.js +3 -2
  9. package/dist/components/Select.d.ts +1 -0
  10. package/dist/components/Select.js +9 -13
  11. package/dist/components/SelectFaceted.d.ts +1 -0
  12. package/dist/components/SelectFaceted.js +29 -23
  13. package/dist/components/select-parts.css +1 -0
  14. package/dist/elements/a-input.css +1 -1
  15. package/dist/elements/a-menu.js +12 -0
  16. package/dist/elements/a-slider.css +1 -1
  17. package/dist/elements/a-tabs.css +1 -1
  18. package/dist/elements/a-tabs.d.ts +3 -0
  19. package/dist/elements/a-tabs.js +40 -1
  20. package/dist/general_types.d.ts +2 -0
  21. package/dist/jsx-runtime.d.ts +9 -0
  22. package/docs/accessibility.md +34 -0
  23. package/docs/changelog.md +551 -0
  24. package/docs/colors.md +42 -0
  25. package/docs/comparison.md +28 -0
  26. package/docs/components/banner.md +267 -0
  27. package/docs/components/button.md +603 -0
  28. package/docs/components/card.md +322 -0
  29. package/docs/components/checkbox.md +297 -0
  30. package/docs/components/dialog.md +314 -0
  31. package/docs/components/expander.md +529 -0
  32. package/docs/components/icon.md +155 -0
  33. package/docs/components/input-autocomplete.md +101 -0
  34. package/docs/components/input-date.md +406 -0
  35. package/docs/components/input-time.md +174 -0
  36. package/docs/components/input.md +610 -0
  37. package/docs/components/loader.md +139 -0
  38. package/docs/components/menu.md +447 -0
  39. package/docs/components/progress.md +182 -0
  40. package/docs/components/radio.md +369 -0
  41. package/docs/components/select-faceted.md +456 -0
  42. package/docs/components/select.md +931 -0
  43. package/docs/components/slider.md +142 -0
  44. package/docs/components/switch.md +198 -0
  45. package/docs/components/tabs.md +471 -0
  46. package/docs/components/tag.md +185 -0
  47. package/docs/components/text.md +148 -0
  48. package/docs/components/title.md +146 -0
  49. package/docs/components/toaster.md +313 -0
  50. package/docs/components/tooltip.md +177 -0
  51. package/docs/credits.md +22 -0
  52. package/docs/index.md +52 -0
  53. package/docs/install-config.md +240 -0
  54. package/docs/normalization.md +105 -0
  55. package/docs/overview.md +10 -0
  56. package/docs/packages/stickers.md +187 -0
  57. package/docs/packages/table.md +153 -0
  58. package/docs/theming.md +13 -0
  59. package/package.json +10 -6
@@ -0,0 +1,139 @@
1
+ # Loader
2
+
3
+ `Loader` is an animated, icon-sized indicator for active work. It rotates while
4
+ the duration is unknown. Pass `value` when the current proportion is known.
5
+
6
+ ## Indeterminate and determinate work
7
+
8
+ ```tsx
9
+ // No value: the gradient communicates active work.
10
+ <Loader label="Loading results" />
11
+
12
+ // A value fills the circle through the current proportion.
13
+ <Loader value={42} label="Uploading files" />
14
+ ```
15
+
16
+ Without `value`, the faded gradient rotates. With `value`, the filled section
17
+ starts at 12 o’clock, has a sharp edge at the current proportion, and stays
18
+ still. The final preview is a slow, docs-only 0–100% cycle. `max` defaults to
19
+ `100`.
20
+
21
+ ## Size and speed
22
+
23
+ ```tsx
24
+ <Loader size={14} speed={1} label="Slow loading" />
25
+ <Loader label="Loading" /> // 16px, 0.75 seconds
26
+ <Loader size={32} label="Loading" />
27
+ ```
28
+
29
+ `size` is a pixel value, matching `Icon`. `speed` is seconds per rotation. All
30
+ loaders with the same speed share a rotation phase.
31
+
32
+ ## Icon-shape alias
33
+
34
+ Use `loader` where an Anta component accepts an `IconShape` string. The alias
35
+ is indeterminate and follows the surrounding icon size and color.
36
+
37
+ ```tsx
38
+ <Button tone="brand" icon="loader" label="Saving" />
39
+ <Tag tone="info" icon="loader" label="Syncing" />
40
+ <Input
41
+ label="Workspace"
42
+ defaultValue="Anta"
43
+ hint="Checking availability"
44
+ dimActions
45
+ trailing={<Icon shape="loader" aria-hidden="true" />}
46
+ />
47
+ ```
48
+
49
+ The alias uses the same conic field as an alpha mask, keeping the icon slot's
50
+ own color. It is decorative; the surrounding control or field provides its
51
+ label, state, and semantics.
52
+
53
+ Use `<Loader>` when you need a value, speed, or accessible loading status.
54
+
55
+ ### Props
56
+
57
+ | Prop | Type | Default | Description |
58
+ |------|------|---------|-------------|
59
+ | `label?` | string | — | Accessible name for a standalone loader. Without a label, the loader is
60
+ decorative so nearby status text remains the single announcement. |
61
+ | `max?` | number | 100 | Upper bound of the range. |
62
+ | `size?` | number | 16 | Width and height in pixels. |
63
+ | `speed?` | number | 0.75 | Seconds per rotation. Pass a positive number. |
64
+ | `value?` | number \| false | — | Current progress value. Omit this prop, or pass `false`, for an
65
+ indeterminate rotating gradient. |
66
+
67
+ Use a raw `<a-loader>` when you are not using the JSX wrapper. Set its size,
68
+ speed, and determinate percentage with CSS custom properties. `value` selects
69
+ the static ring; `--loader-value` is the percentage it shows.
70
+
71
+ ```html
72
+ <!-- Indeterminate -->
73
+ <a-loader
74
+ role="progressbar"
75
+ aria-label="Loading files"
76
+ style="--loader-size: 24px; --loader-speed: 1s"
77
+ ></a-loader>
78
+
79
+ <!-- Determinate -->
80
+ <a-loader
81
+ value="42"
82
+ role="progressbar"
83
+ aria-label="Uploading files"
84
+ aria-valuenow="42"
85
+ aria-valuemin="0"
86
+ aria-valuemax="100"
87
+ style="--loader-size: 24px; --loader-value: 42%"
88
+ ></a-loader>
89
+ ```
90
+
91
+ When the value is unknown, omit `value` and give a standalone loader an
92
+ accessible name. Mark a loader beside visible status text with `aria-hidden`.
93
+
94
+ The standalone Loader uses the quiet `--text-3` color by default. Set `color`
95
+ directly when it needs a deliberate semantic or one-off color; the `loader`
96
+ icon alias inherits its parent control’s color.
97
+
98
+ ```css
99
+ .loader-demo { color: mediumaquamarine; }
100
+ ```
101
+
102
+ The `.loader-demo` class is only for this preview. Use a color on a parent or
103
+ your own selector in an application stylesheet.
104
+
105
+ ```css
106
+ a-loader.rainbow-loader:not([value]) {
107
+ background: conic-gradient(
108
+ from 0deg,
109
+ color-mix(in oklch, #ff5d8f 8%, transparent),
110
+ #ff5d8f 25%,
111
+ #9b6cf5 50%,
112
+ #55b8e8 75%,
113
+ #45ca9f
114
+ );
115
+ }
116
+ ```
117
+
118
+ Override `background` to tune an indeterminate gradient field. Keep the
119
+ opening stop faint so the rotating hand still has a readable leading edge.
120
+
121
+ ```css
122
+ a-loader.ring-loader {
123
+ position: relative;
124
+ color: var(--anta-seed-brand);
125
+ }
126
+
127
+ a-loader.ring-loader::after {
128
+ content: "";
129
+ position: absolute;
130
+ inset: 5px;
131
+ border-radius: 100%;
132
+ background: var(--bg-2); /* the known surface behind the Loader */
133
+ pointer-events: none;
134
+ }
135
+ ```
136
+
137
+ **Known surface.** This paints a `--bg-2` circle over the Loader, matching
138
+ this preview’s explicit background and making a visual hole. Use the same
139
+ approach only when your Loader sits on a known opaque surface.
@@ -0,0 +1,447 @@
1
+ # Menu
2
+
3
+ A dropdown or context menu that anchors to any target and just works. Place
4
+ `<Menu>` immediately after the trigger — a button, an icon, a whole region —
5
+ and it opens on click, positions itself with room-aware flipping, and dismisses
6
+ on outside-click, `Escape`, or scroll. Rows are dedicated **`MenuItem`s** (not
7
+ buttons), with optional leading icons, trailing keyboard hints, separators,
8
+ grouped sections, and nested submenus.
9
+
10
+ Open state is uncontrolled by default — pass **`onStateChange`** (event-first:
11
+ `(event, { next, prev })`, or listen for the raw **`statechange`** event) to
12
+ observe it, or pass **`open`** to control it, applying `detail.next` to `open`
13
+ from your `onStateChange` handler (submenus stay uncontrolled). `statechange` is
14
+ **cancelable** and fires before the menu moves, so uncontrolled you can keep it
15
+ open with `event.preventDefault()`. You can also grab a `ref` and call
16
+ `.open()` / `.close()` / `.toggle()`.
17
+
18
+ ## Dropdown from a button
19
+
20
+ Render `<Menu>` right after the trigger element. A `MenuItem` takes a leading
21
+ `icon`, a `label` (or `children`), and a trailing `kbd` hint. Selecting an item
22
+ closes the menu and fires its `onSelect(event, { value, label })`; `Escape` or an
23
+ outside click dismisses without selecting. Give items a `value` to route a single
24
+ handler — `onSelect` reports the chosen item's `value` and `label`, and never
25
+ fires for a submenu parent (that opens the flyout) or a selection bubbling up
26
+ from a nested submenu.
27
+
28
+ ```tsx
29
+ // One handler, keyed by each item's `value`.
30
+ const onSelect = (e, { value }) => act(value)
31
+
32
+ <Button>Actions</Button>
33
+ <Menu>
34
+ <MenuItem icon="edit" label="Edit" kbd="⌘E" value="edit" onSelect={onSelect} />
35
+ <MenuItem icon="copy" label="Duplicate" kbd="⌘D" value="duplicate" onSelect={onSelect} />
36
+ <MenuItem icon="download" label="Export" value="export" onSelect={onSelect} />
37
+ <MenuSeparator />
38
+ <MenuItem tone="critical" icon="trash" label="Delete" kbd="⌘⌫" value="delete" onSelect={onSelect} />
39
+ </Menu>
40
+ ```
41
+
42
+ ## Tones
43
+
44
+ `tone` colors an item's label, icon, and hover/selected tint (and its
45
+ `selectionIndicator`) with a semantic palette — `brand`, `info`, `success`,
46
+ `warning`, or `critical` — tracking light and dark mode automatically. Omit it (or
47
+ pass `neutral`) for the standard gray. `critical` is the conventional destructive
48
+ action. Pass **any CSS color** for a one-off custom tone — its hue and chroma are
49
+ kept while the lightness is pinned to match the brand text (resolved through
50
+ `--menu-item-tone-source`).
51
+
52
+ `toneSelected` applies the tone the same way, but **only while the row is `selected`**
53
+ — an unselected row stays neutral. It's the checkable-menu counterpart to `tone`
54
+ (`Select` uses it to tone the chosen row).
55
+
56
+ ```tsx
57
+ <Menu>
58
+ <MenuItem label="Neutral" />
59
+ <MenuItem tone="brand" icon="star" label="Brand" />
60
+ <MenuItem tone="info" icon="info" label="Info" />
61
+ <MenuItem tone="success" icon="circle-check" label="Success" />
62
+ <MenuItem tone="warning" icon="warning-triangle" label="Warning" />
63
+ <MenuItem tone="critical" icon="trash" label="Critical" />
64
+ </Menu>
65
+ ```
66
+
67
+ ## Placement
68
+
69
+ `placement` sets the preferred side and edge alignment: `bottom-start` (the
70
+ default), `bottom-end`, `top-start`, or `top-end`. The `-start` / `-end` suffix
71
+ aligns the menu's left or right edge to the trigger. The menu auto-flips
72
+ vertically and clamps horizontally when there isn't room, so the placement is a
73
+ preference, not a constraint.
74
+
75
+ ```tsx
76
+ <Button>bottom-start</Button>
77
+ <Menu placement="bottom-start">…</Menu>
78
+
79
+ <Button>bottom-end</Button>
80
+ <Menu placement="bottom-end">…</Menu>
81
+
82
+ <Button>top-start</Button>
83
+ <Menu placement="top-start">…</Menu>
84
+
85
+ <Button>top-end</Button>
86
+ <Menu placement="top-end">…</Menu>
87
+ ```
88
+
89
+ The menu positions against the trigger's box. A trigger can narrow that: if it
90
+ implements **`getAnchorRect(): DOMRect`**, the menu positions against *that* rect
91
+ instead of its full border box. Anta's `Input` does this — it reports its field
92
+ box — so a `Menu` placed after an `<Input>` (as in `Select`) lines up with the
93
+ field, not the whole component (whose box also spans the label and hint).
94
+
95
+ ## Right-click & at-cursor
96
+
97
+ Pass `context` to open on right-click (the `contextmenu` event) of the region
98
+ the menu follows, positioned at the pointer — ideal for content surfaces like a
99
+ log viewer or canvas where secondary actions shouldn't clutter the UI. `coord`
100
+ on its own opens a normal left-click menu at the cursor instead of aligned to
101
+ the trigger box.
102
+
103
+ ```tsx
104
+ <div class="canvas">Right-click anywhere here</div>
105
+ <Menu context>
106
+ <MenuItem icon="link" label="Share log" onSelect={share} />
107
+ <MenuItem icon="copy" label="Copy moment" onSelect={copy} />
108
+ </Menu>
109
+ ```
110
+
111
+ ## Submenus
112
+
113
+ Mark a `MenuItem` with `submenu` and nest a `<Menu>` inside it to create a
114
+ flyout (the nested menu detects it's a submenu from being inside the item). The
115
+ parent item gets a chevron (override it per item with `iconTrailing`),
116
+ `aria-haspopup="menu"`, and an `aria-expanded` that
117
+ tracks the flyout's open state; the submenu opens
118
+ to the side and flips when it nears the edge. Submenus open on **hover**
119
+ (with intent timing, so a quick pass-through doesn't trigger them) as well as on
120
+ click; add `nohover` to a submenu to make it click-only. Hover-intent is
121
+ mouse-only regardless: on touch a submenu opens on tap and stays open until you
122
+ dismiss it or open a sibling.
123
+
124
+ For a top-level dropdown, add `aria-haspopup="menu"` to your own trigger element
125
+ — it's a control you render, so the accessible name and popup hint live there.
126
+
127
+ Submenus nest arbitrarily deep — here `View → Columns → Visible columns` is three
128
+ flyouts in. The deepest one isn't `MenuItem`s but **custom content**: a checkbox
129
+ list whose region carries `data-menu-open`, so ticking a box (or the **Select all**
130
+ toggle, or a row's **Only** button — which leaves just that column on) updates state
131
+ **without closing the menu**.
132
+
133
+ ```tsx
134
+ const COLUMNS = ['Name', 'Status', 'Owner', 'Created', 'Modified']
135
+ const [on, setOn] = useState(() => Object.fromEntries(COLUMNS.map((c) => [c, true])))
136
+ const allOn = COLUMNS.every((c) => on[c])
137
+ const someOn = COLUMNS.some((c) => on[c])
138
+ const only = (c) => setOn(Object.fromEntries(COLUMNS.map((k) => [k, k === c])))
139
+ // …toggle / toggleAll omitted
140
+
141
+ <Button>More</Button>
142
+ <Menu>
143
+ <MenuItem icon="edit" label="Rename" />
144
+ <MenuItem label="Move to" submenu>
145
+ <Menu>
146
+ <MenuItem icon="folder-open" label="Projects" />
147
+ <MenuItem icon="folder-open" label="Archive" />
148
+ </Menu>
149
+ </MenuItem>
150
+ <MenuItem label="View" submenu>
151
+ <Menu>
152
+ <MenuItem label="Columns" submenu>
153
+ <Menu>
154
+ <MenuItem label="Visible columns" submenu>
155
+ <Menu>
156
+ {/* custom content — never auto-closes thanks to data-menu-open */}
157
+ <div class="menu-check-row" data-menu-open>
158
+ {/* Checkbox takes 'indeterminate' directly — no ref poking */}
159
+ <Checkbox
160
+ className="menu-check"
161
+ label="Select all"
162
+ checked={allOn ? true : someOn ? 'indeterminate' : false}
163
+ onStateChange={toggleAll}
164
+ />
165
+ </div>
166
+ <MenuSeparator />
167
+ {COLUMNS.map((c) => (
168
+ <div class="menu-check-row" data-menu-open key={c}>
169
+ <Checkbox className="menu-check" label={c} checked={on[c]} onStateChange={() => toggle(c)} />
170
+ <Button priority="tertiary" size="small" onClick={() => only(c)}>Only</Button>
171
+ </div>
172
+ ))}
173
+ </Menu>
174
+ </MenuItem>
175
+ </Menu>
176
+ </MenuItem>
177
+ </Menu>
178
+ </MenuItem>
179
+ </Menu>
180
+ ```
181
+
182
+ ## Groups
183
+
184
+ Wrap related items in a `MenuGroup` with a `label` to add a titled section.
185
+ Headings are skipped during keyboard navigation, which flattens all items into a
186
+ single up/down sequence across group boundaries.
187
+
188
+ A `MenuSeparator` is a plain hairline when empty, but given text
189
+ (`<MenuSeparator>No results</MenuSeparator>`) it renders a small muted caption and
190
+ becomes an `aria-live="polite"` status region — the styled home for a status or
191
+ empty-state message (what `Select`'s `renderEmpty` uses).
192
+
193
+ ```tsx
194
+ <Menu>
195
+ <MenuGroup label="Sort by">
196
+ <MenuItem icon="check" label="Name" />
197
+ <MenuItem label="Date modified" />
198
+ </MenuGroup>
199
+ <MenuSeparator />
200
+ <MenuGroup label="Show">
201
+ <MenuItem label="Hidden files" data-menu-open />
202
+ </MenuGroup>
203
+ </Menu>
204
+ ```
205
+
206
+ ## Pinned header & footer
207
+
208
+ `a-menu-header` and `a-menu-footer` name fixed regions around the scrolling
209
+ items. Put `a-select-header` or `a-select-footer` in the same slots when a
210
+ composed Select needs its own DOM anatomy. A `MenuSeparator` slotted into the
211
+ footer stays the menu's divider.
212
+
213
+ ```tsx
214
+ <Button>Filter</Button>
215
+ <Menu inset={8}>
216
+ <a-menu-header slot="header" data-menu-open>
217
+ <Input size="small" placeholder="Filter…" />
218
+ </a-menu-header>
219
+ <MenuItem label="Design" />
220
+ <MenuItem label="Engineering" />
221
+ <MenuSeparator slot="footer" />
222
+ <a-menu-footer slot="footer">
223
+ <MenuItem icon="x" label="Clear filters" data-menu-open />
224
+ </a-menu-footer>
225
+ </Menu>
226
+ ```
227
+
228
+ ## Custom content & the close contract
229
+
230
+ The close decision is read straight from the DOM on click — never from whether a
231
+ handler called `preventDefault` — so it's safe in runtimes where event handlers
232
+ run off the UI thread. Walking out from the click, the nearest marker wins:
233
+
234
+ - A **`MenuItem`** is a choice → selecting it **closes** the menu.
235
+ - **Arbitrary content you inject never closes it** — drop a slider, an input, or
236
+ any control straight into a `<Menu>` and interacting leaves the menu open.
237
+ - **`data-menu-open`** keeps the menu open past a click —
238
+ put it on an item, a `MenuGroup`, or any wrapping element (toggles, multi-select).
239
+ - **`data-menu-close`** opts arbitrary content *into* closing — so a custom
240
+ element (or a "Done" button inside a `data-menu-open` region) can dismiss the
241
+ menu without being a `MenuItem`.
242
+
243
+ For programmatic open/close from app state, drive the `open` prop (the `state`
244
+ attribute, `"open"` / `"closed"`) and react via `onStateChange`, or grab a `ref`
245
+ and call `.open()` / `.close()` / `.toggle()`.
246
+
247
+ ```tsx
248
+ <Menu>
249
+ <MenuItem label="Profile" onSelect={openProfile} />
250
+ {/* data-menu-open: a toggle that doesn't dismiss */}
251
+ <MenuItem label="Notifications" data-menu-open onSelect={toggleNotifs} />
252
+ <MenuSeparator />
253
+ {/* data-menu-open region: interacting with the slider keeps the menu open;
254
+ the Done button opts back into closing with data-menu-close */}
255
+ <div data-menu-open class="zoom">
256
+ <label>Zoom</label>
257
+ <input type="range" />
258
+ {/* a plain button, not a MenuItem — data-menu-close lets it dismiss */}
259
+ <button data-menu-close>Done</button>
260
+ </div>
261
+ </Menu>
262
+ ```
263
+
264
+ ## Links
265
+
266
+ Give a `MenuItem` an **`href`** and it renders a native `<a>` instead of the
267
+ custom element — a real link. It navigates on click, opens in a new tab on
268
+ ⌘ / middle-click, offers "copy link address", and takes the anchor attributes:
269
+ `target`, `rel`, `ping`, and **`download`** (`true` for the resource's default
270
+ filename, a string to rename it). The parent `Menu` treats it as a first-class
271
+ row — arrow-key navigation, Enter / Space activation, and close-on-select — mixed
272
+ freely with regular items. `onSelect` fires alongside the navigation; `disabled`
273
+ drops the `href` so it can't navigate; `selected` marks a current link
274
+ (`aria-current`).
275
+
276
+ Under the hood it's `<a role="menuitem" data-anta-menu-item href>`, the same
277
+ opt-in-marker pattern as [`Button`'s link mode](./button.md#link-mode).
278
+
279
+ ```tsx
280
+ <Menu>
281
+ {/* Real links — open in a new tab, or download */}
282
+ <MenuItem icon="file" label="Documentation" href="/docs" target="_blank" iconTrailing="external-link" />
283
+ <MenuItem icon="download" label="Download report" href="/report.pdf" download="report.pdf" />
284
+ <MenuSeparator />
285
+ {/* A regular action row, mixed in */}
286
+ <MenuItem icon="settings" label="Preferences" onSelect={openPrefs} />
287
+ </Menu>
288
+ ```
289
+
290
+ ## Selected
291
+
292
+ `selected` gives a row a persistent background tint — the resting fill a pressed
293
+ row shows — marking it as chosen. It touches only the background, so nothing
294
+ shifts and no gutter is reserved. It's the building block for select-style
295
+ menus: mark one row for single-select, several for multi-select (pair with
296
+ `data-menu-open` to keep the menu open as choices toggle). The tint tracks
297
+ `tone`, so a toned row stays in its own color.
298
+
299
+ For a real checkable row, add **`selectionIndicator`** (`'checkbox'` or `'radio'`).
300
+ It renders a passive checkbox/radio at the leading edge (reusing the
301
+ [Checkbox](./checkbox.md) / [Radio](./radio.md) element visuals),
302
+ flips the row to `role="menuitemcheckbox"` / `"menuitemradio"`, and pairs
303
+ `aria-checked` — the row stays the control, the indicator is decorative. This is
304
+ what [Select](./select.md)'s `selection` modes render; use it directly for
305
+ a checkable menu.
306
+
307
+ ```tsx
308
+ {/* Plain tint (no indicator) */}
309
+ <MenuItem label="Relevance" selected />
310
+
311
+ {/* Checkable rows — the row is the control; the mark is decorative */}
312
+ <MenuItem selectionIndicator="radio" label="Relevance" selected />
313
+ <MenuItem selectionIndicator="checkbox" label="Show archived" selected data-menu-open />
314
+ ```
315
+
316
+ ## Badges, counters & hints
317
+
318
+ Drop a `<Tag>` (or any element) as a **child** of a `MenuItem` for a trailing
319
+ counter or badge. Children render after the label but before the `kbd` hint and
320
+ trailing icon, and the label's `flex: 1` right-aligns them — so a small tag sits
321
+ at the row's edge, just left of a shortcut or submenu chevron. Use `size="small"`
322
+ so it sits comfortably in the row.
323
+
324
+ Add `hint` for secondary text under the label — muted, and it tracks the row's
325
+ `tone`. It stacks in a column beneath the label while the icon, badge, and
326
+ chevron stay centered on the row, so it composes with a trailing counter.
327
+
328
+ ```tsx
329
+ <Menu>
330
+ <MenuItem icon="folder-open" label="Active" hint="12 open items"><Tag size="small" tone="info">12</Tag></MenuItem>
331
+ <MenuItem icon="folder-close" label="Archived" hint="Read-only" />
332
+ <MenuItem icon="trash" label="Trash"><Tag size="small">3</Tag></MenuItem>
333
+ </Menu>
334
+ ```
335
+
336
+ `MenuItemCopy` is a row that copies to the clipboard when chosen — `copy` for a
337
+ literal string, `copyNode` for a DOM region. It composes a `<MenuItem>` with a
338
+ slotted `<a-copy>` element that performs the write, and keeps the menu open on
339
+ select so the check / retone feedback shows. `onCopied(ok)` fires after each
340
+ attempt.
341
+
342
+ Copy sits alongside normal items, so a "Share" menu can mix a copy-link row with
343
+ navigating links.
344
+
345
+ ```tsx
346
+ <Button icon="share" iconTrailing="chevron-down" label="Share" />
347
+ <Menu>
348
+ <MenuItemCopy copy={pageUrl} label="Copy link" kbd="⌘C" />
349
+ <MenuItemCopy copy={embedHtml} icon="braces" label="Copy embed" />
350
+ <MenuSeparator />
351
+ <MenuItem icon="external-link" label="Open in new tab" href="/menu" target="_blank" />
352
+ </Menu>
353
+ ```
354
+
355
+ ### Props
356
+
357
+ | Prop | Type | Default | Description |
358
+ |------|------|---------|-------------|
359
+ | `copy?` | string | — | Text copied to the clipboard on activation. |
360
+ | `copyNode?` | boolean \| string | — | Copy a DOM node as rich text (`text/html`) + plain text. `true` copies
361
+ the nearest ancestor marked `data-copy-source`; a string is a CSS
362
+ selector for an ancestor region (`closest`). The copy control is stripped
363
+ from the copied output. |
364
+ | `copyUrl?` | true | — | Copy the current page URL (`location.href`). |
365
+ | `copyWithUrl?` | boolean | — | Prefix the copied text with `// URL: <current page URL>`. |
366
+ | `onCopied?` | (ok) => void | — | Fires after the copy attempt with whether it succeeded. |
367
+ | `onCopyRequest?` | () => void | — | Compute the copy content lazily. Fires on pointerdown / keydown; update
368
+ `copy` (a state change) here and the activation copies the latest value.
369
+ The gap lets the update land even off the UI thread — only the
370
+ serializable `copy` string crosses. |
371
+
372
+ ## Keyboard
373
+
374
+ While open, the menu keeps Tab focus contained. Open the trigger with `Enter` /
375
+ `Space` to focus the first item.
376
+
377
+ - **↑ / ↓** — move between items (wraps; skips separators, headings, and disabled items)
378
+ - **Home / End** — first / last item
379
+ - **Type-ahead** — type to jump to the next item whose label matches
380
+ - **→ / Enter** — open a submenu (and focus its first item); **←** — close it, back to the parent
381
+ - **Enter / Space** — activate the focused item
382
+ - **Tab / Shift+Tab** — cycle through the focusables *inside* the menu — items **and** any nested controls (a slider, an input, a button) — wrapping at the ends so focus stays in the open menu. Arrow keys hand off to a nested control once it's focused
383
+ - **Esc** — close the topmost menu and return focus to its parent item or trigger
384
+
385
+ ## Controlled vs Uncontrolled
386
+
387
+ Open state follows Anta's shared **state contract**. By default the menu is
388
+ *uncontrolled* — its triggers (click / right-click / `.open()`) own open/close,
389
+ so there's no prop to set. Pass `open` to *control* it: visibility follows the
390
+ prop, and a user dismiss only *requests* a change. The `<Menu>` wrapper takes a
391
+ boolean; the `<a-menu>` element takes a `state` string enum:
392
+
393
+ | | `<Menu>` (JSX) | `<a-menu>` (element) |
394
+ |---|---|---|
395
+ | **Uncontrolled** (default) — triggers own it | *(no prop)* | *(no `state` attribute)* |
396
+ | **Controlled** — you own it | `open` + `onStateChange` | `state="open" \| "closed"` + `statechange` |
397
+ | **Change event** | `onStateChange(event, { next, prev })` — `next` / `prev` are **booleans** | `statechange`, a `CustomEvent<{ next, prev }>` — `'open'` / `'closed'` (plus `coord` / `originEvent`) |
398
+
399
+ `statechange` is **cancelable** and fires *before* the menu moves. Uncontrolled,
400
+ call `event.preventDefault()` to keep the menu as-is (e.g. block a dismiss).
401
+ Controlled, the menu never self-moves — apply `detail.next` to `open` to accept,
402
+ or do nothing to reject. (Submenus are always uncontrolled, regardless of `open`.)
403
+
404
+ Use this when you are not using the React or Preact wrapper and a native HTML control does not fit: construct the equivalent Anta web component from the elements below.
405
+
406
+ Put a focusable trigger immediately before `<a-menu>` and omit `state` for an
407
+ uncontrolled menu. Its light-DOM `<a-menu-item>` rows need their own `tabindex`
408
+ values.
409
+
410
+ ```html
411
+ <a-button role="button" tabindex="0" aria-haspopup="menu">
412
+ <a-button-label>Actions</a-button-label>
413
+ </a-button>
414
+ <a-menu role="menu">
415
+ <a-menu-item role="menuitem" tabindex="0" value="settings"><a-menu-item-label>Settings</a-menu-item-label></a-menu-item>
416
+ <a-menu-item role="menuitem" tabindex="0" value="members"><a-menu-item-label>Members</a-menu-item-label></a-menu-item>
417
+ </a-menu>
418
+ ```
419
+
420
+ The popover surface lives in shadow DOM and is exposed as a **part** — style its
421
+ chrome (background, frost, border, radius, shadow, padding, min-width) with
422
+ `::part(menu)`. Menu items are light DOM, so style them (and their
423
+ `a-icon` / `kbd` / `a-menu-item-label` children) directly. Match **both** item
424
+ shapes — the `a-menu-item` custom element and the `a[data-anta-menu-item]` link —
425
+ with `:is(a-menu-item, a[data-anta-menu-item])`, or a link row skips your item
426
+ rule. **Click the trigger** to open the styled menu; the `.fancy` class is just
427
+ for the demo:
428
+
429
+ ```css
430
+ /* a "liquid glass" menu — more transparent + a stronger blur, big radius, fully
431
+ rounded items with roomier padding and bigger icons / type. */
432
+ a-menu.fancy::part(menu) {
433
+ border-radius: 22px;
434
+ min-width: 240px;
435
+ padding: 8px;
436
+ background: light-dark(rgba(255,255,255,0.55), rgba(28,26,32,0.5));
437
+ backdrop-filter: blur(28px) saturate(180%);
438
+ }
439
+ /* Items are light DOM — style them directly. `:is()` catches both an
440
+ <a-menu-item> and a link row (<a data-anta-menu-item>). */
441
+ a-menu.fancy :is(a-menu-item, a[data-anta-menu-item]) {
442
+ border-radius: 999px;
443
+ padding: 10px 16px;
444
+ font-size: 15px;
445
+ --icon-size: 18px; /* inherited by the item's <a-icon> */
446
+ }
447
+ ```