@antadesign/anta 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/NOTICES.md +32 -1
  2. package/dist/anta_helpers.d.ts +25 -0
  3. package/dist/anta_helpers.js +28 -0
  4. package/dist/box-observation.d.ts +8 -0
  5. package/dist/box-observation.js +41 -0
  6. package/dist/box-types.d.ts +2 -0
  7. package/dist/bundle.css +1 -1
  8. package/dist/bundle.js +77 -46
  9. package/dist/components/Banner.js +2 -2
  10. package/dist/components/Box.d.ts +28 -12
  11. package/dist/components/Box.js +18 -11
  12. package/dist/components/Breadcrumbs.css +1 -1
  13. package/dist/components/Button.js +2 -2
  14. package/dist/components/Calendar.js +3 -5
  15. package/dist/components/Card.js +2 -2
  16. package/dist/components/Checkbox.d.ts +12 -13
  17. package/dist/components/Checkbox.js +5 -13
  18. package/dist/components/Expander.js +2 -2
  19. package/dist/components/Input.d.ts +8 -2
  20. package/dist/components/Input.js +6 -2
  21. package/dist/components/InputAutocomplete.js +3 -12
  22. package/dist/components/InputDate.css +1 -1
  23. package/dist/components/InputDate.js +2 -0
  24. package/dist/components/InputTime.js +2 -2
  25. package/dist/components/Loader.js +2 -1
  26. package/dist/components/Menu.d.ts +4 -4
  27. package/dist/components/Menu.js +1 -1
  28. package/dist/components/MenuItem.d.ts +7 -8
  29. package/dist/components/MenuItem.js +4 -4
  30. package/dist/components/Panel.d.ts +17 -0
  31. package/dist/components/Panel.js +22 -0
  32. package/dist/components/Progress.js +2 -2
  33. package/dist/components/RadioGroup.d.ts +14 -14
  34. package/dist/components/RadioGroup.js +7 -23
  35. package/dist/components/Select.d.ts +26 -21
  36. package/dist/components/Select.js +51 -38
  37. package/dist/components/SelectFaceted.d.ts +24 -11
  38. package/dist/components/SelectFaceted.js +65 -44
  39. package/dist/components/Slider.js +3 -3
  40. package/dist/components/Steps.css +1 -1
  41. package/dist/components/Switch.d.ts +10 -11
  42. package/dist/components/Switch.js +3 -11
  43. package/dist/components/TabPanel.d.ts +3 -8
  44. package/dist/components/TabPanel.js +2 -1
  45. package/dist/components/Tabs.d.ts +2 -2
  46. package/dist/components/Tabs.js +4 -3
  47. package/dist/components/Tag.d.ts +7 -9
  48. package/dist/components/Tag.js +2 -2
  49. package/dist/components/Text.js +2 -2
  50. package/dist/components/Title.js +2 -2
  51. package/dist/components/Tooltip.d.ts +1 -1
  52. package/dist/components/Tooltip.js +1 -1
  53. package/dist/components/select-parts.css +1 -1
  54. package/dist/elements/a-avatar.css +1 -1
  55. package/dist/elements/a-banner.css +1 -1
  56. package/dist/elements/a-box.css +1 -1
  57. package/dist/elements/a-box.d.ts +6 -6
  58. package/dist/elements/a-box.js +74 -23
  59. package/dist/elements/a-button.css +1 -1
  60. package/dist/elements/a-button.d.ts +16 -10
  61. package/dist/elements/a-button.js +64 -1
  62. package/dist/elements/a-calendar.css +1 -1
  63. package/dist/elements/a-capture.css +1 -1
  64. package/dist/elements/a-card.css +1 -1
  65. package/dist/elements/a-checkbox.css +1 -1
  66. package/dist/elements/a-checkbox.d.ts +3 -0
  67. package/dist/elements/a-checkbox.js +17 -1
  68. package/dist/elements/a-copy.css +1 -1
  69. package/dist/elements/a-dialog.css +1 -1
  70. package/dist/elements/a-expander.css +1 -1
  71. package/dist/elements/a-expander.js +1 -0
  72. package/dist/elements/a-icon.css +1 -1
  73. package/dist/elements/a-icon.shapes.css +1 -1
  74. package/dist/elements/a-input-time.css +1 -1
  75. package/dist/elements/a-input-time.d.ts +8 -0
  76. package/dist/elements/a-input-time.js +120 -15
  77. package/dist/elements/a-input.css +1 -1
  78. package/dist/elements/a-input.d.ts +27 -1
  79. package/dist/elements/a-input.js +180 -41
  80. package/dist/elements/a-loader.css +1 -1
  81. package/dist/elements/a-menu-group.css +1 -1
  82. package/dist/elements/a-menu-item.css +1 -1
  83. package/dist/elements/a-menu-item.js +1 -0
  84. package/dist/elements/a-menu-separator.css +1 -1
  85. package/dist/elements/a-menu.css +1 -1
  86. package/dist/elements/a-menu.d.ts +8 -13
  87. package/dist/elements/a-menu.js +146 -45
  88. package/dist/elements/a-panel.css +1 -0
  89. package/dist/elements/a-panel.d.ts +16 -0
  90. package/dist/elements/a-panel.js +153 -0
  91. package/dist/elements/a-progress.css +1 -1
  92. package/dist/elements/a-radio-group.css +1 -1
  93. package/dist/elements/a-radio.css +1 -1
  94. package/dist/elements/a-radio.d.ts +4 -0
  95. package/dist/elements/a-radio.js +18 -0
  96. package/dist/elements/a-slider.css +1 -1
  97. package/dist/elements/a-slider.d.ts +1 -0
  98. package/dist/elements/a-slider.js +3 -6
  99. package/dist/elements/a-switch.css +1 -1
  100. package/dist/elements/a-switch.d.ts +3 -0
  101. package/dist/elements/a-switch.js +17 -1
  102. package/dist/elements/a-tab.css +1 -1
  103. package/dist/elements/a-tabpanel.css +1 -1
  104. package/dist/elements/a-tabs.css +1 -1
  105. package/dist/elements/a-tag.css +1 -1
  106. package/dist/elements/a-text.css +1 -1
  107. package/dist/elements/a-text.js +1 -0
  108. package/dist/elements/a-title.css +1 -1
  109. package/dist/elements/a-toast.css +1 -1
  110. package/dist/elements/a-toaster.css +1 -1
  111. package/dist/elements/a-tooltip.css +1 -1
  112. package/dist/elements/a-tooltip.d.ts +3 -4
  113. package/dist/elements/a-tooltip.js +30 -8
  114. package/dist/elements/focus.d.ts +9 -0
  115. package/dist/elements/focus.js +41 -0
  116. package/dist/elements/index.d.ts +1 -0
  117. package/dist/elements/index.js +3 -0
  118. package/dist/elements/panel-focus.d.ts +10 -0
  119. package/dist/elements/panel-focus.js +86 -0
  120. package/dist/elements/shadow-aria.d.ts +10 -0
  121. package/dist/elements/shadow-aria.js +155 -0
  122. package/dist/general_types.d.ts +70 -42
  123. package/dist/generate-icons.mjs +3 -0
  124. package/dist/index.d.ts +5 -2
  125. package/dist/index.js +2 -0
  126. package/dist/jsx-runtime.d.ts +10 -1
  127. package/dist/reset.css +1 -1
  128. package/dist/theme-antithesis.css +1 -0
  129. package/dist/theme-antune.css +1 -0
  130. package/dist/tokens.css +1 -1
  131. package/docs/accessibility.md +4 -2
  132. package/docs/changelog.md +135 -0
  133. package/docs/colors.md +7 -7
  134. package/docs/comparison.md +2 -2
  135. package/docs/components/box.md +98 -34
  136. package/docs/components/breadcrumbs.md +75 -6
  137. package/docs/components/button.md +2 -2
  138. package/docs/components/capture.md +8 -8
  139. package/docs/components/checkbox.md +21 -16
  140. package/docs/components/input-autocomplete.md +6 -0
  141. package/docs/components/input-date.md +8 -2
  142. package/docs/components/input.md +99 -40
  143. package/docs/components/menu.md +5 -6
  144. package/docs/components/panel.md +189 -0
  145. package/docs/components/radio.md +16 -13
  146. package/docs/components/select-faceted.md +58 -2
  147. package/docs/components/select.md +72 -59
  148. package/docs/components/steps.md +4 -4
  149. package/docs/components/switch.md +16 -10
  150. package/docs/components/tabs.md +21 -8
  151. package/docs/components/tag.md +5 -3
  152. package/docs/components/tooltip.md +6 -4
  153. package/docs/index.md +1 -0
  154. package/docs/install-config.md +23 -12
  155. package/docs/normalization.md +6 -4
  156. package/docs/theming.md +95 -17
  157. package/package.json +8 -4
  158. package/dist/components/InputDate.module.css +0 -1
  159. package/dist/components/Select.module.css +0 -1
  160. package/dist/components/SelectFaceted.module.css +0 -1
  161. package/dist/search-index.json +0 -0
  162. package/dist/theme-anta.css +0 -1
@@ -5,32 +5,34 @@ events. For custom wheel, pointer, and touch handling, use [Capture](./capture.m
5
5
 
6
6
  ## Display
7
7
 
8
- Use `display`, `gap`, and `round` for layout and corners. Numeric lengths use
9
- pixels; strings accept CSS lengths. A bare `round` fully rounds the corners.
8
+ Use `display`, `gap`, `padding`, `margin`, and `round` for layout and corners. Numeric lengths use
9
+ pixels; strings accept CSS lengths. `padding` and `margin` also accept CSS
10
+ shorthand, such as `padding="8px 16px"` or `margin="0 auto"`. Omission adds no
11
+ spacing styles. A bare `round` fully rounds the corners.
10
12
 
11
13
  ```tsx
12
- <Box round={8}><span /></Box>
13
- <Box display="flex" round={8} gap={6}><span /></Box>
14
- <Box display="grid" round={8} gap="0.5rem" style={{ gridTemplateColumns: '1fr 1fr' }}><span /></Box>
14
+ <Box round={8} padding={10}><span /></Box>
15
+ <Box display="flex" round={8} gap={6} padding={10}><span /></Box>
16
+ <Box display="grid" round={8} gap="0.5rem" padding="8px 16px" style={{ gridTemplateColumns: '1fr 1fr' }}><span /></Box>
15
17
  ```
16
18
 
17
19
  ## Overflow
18
20
 
19
21
  Box reports overflow, clipping, and scrollability as measurements and CSS
20
- states. Use `observe="size"` to keep CSS states current. `fade` and
21
- `onMeasureChange` also enable measurement; without these, Box adds no size
22
- observers.
22
+ states. Use `observe="overflow"` to keep overflow, clipping, and scrollability
23
+ states current as content changes. `observe="edges"` also tracks which
24
+ edges hide content while scrolling. `fade` enables both automatically.
23
25
 
24
26
  `.edge` is a demo class name. Use your own selector.
25
27
 
26
28
  ```tsx
27
- <Box observe="size" round={8} className="edge" style={{ width: 150 }}>Content that fits.</Box>
29
+ <Box observe="overflow" round={8} className="edge" style={{ width: 150 }}>Content that fits.</Box>
28
30
 
29
- <Box observe="size" round={8} className="edge" style={{ width: 150, overflow: 'hidden', whiteSpace: 'nowrap' }}>
31
+ <Box observe="overflow" round={8} className="edge" style={{ width: 150, overflow: 'hidden', whiteSpace: 'nowrap' }}>
30
32
  A label too long for this box.
31
33
  </Box>
32
34
 
33
- <Box observe="size" round={8} className="edge" style={{ width: 150, height: 56, overflowY: 'auto' }}>
35
+ <Box observe="overflow" round={8} className="edge" style={{ width: 150, height: 56, overflowY: 'auto' }}>
34
36
  One. Two. Three. Four. Five. Six. Seven. Eight. Nine. Ten.
35
37
  </Box>
36
38
  ```
@@ -65,7 +67,7 @@ const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
65
67
  The mask clips to the padding box. It preserves the Box border, shadows, and
66
68
  focus ring when no edge is hidden.
67
69
 
68
- To style hidden edges without `fade`, use `observe="size"` and the
70
+ To style hidden edges without `fade`, use `observe="edges"` and the
69
71
  `hidden-start-x`, `hidden-end-x`, `hidden-start-y`, or `hidden-end-y` CSS states.
70
72
 
71
73
  ### Tooltip on clipped content
@@ -88,11 +90,63 @@ const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
88
90
 
89
91
  ## Measurements
90
92
 
91
- `onMeasureChange` reports one frame after observation starts and when the Box or
92
- its content changes. `changed` contains changed fields; `current` is the full
93
- snapshot. Reporting pauses off screen and resumes when Box returns.
94
-
95
- Resize or scroll the Box to update its measurements.
93
+ By default, `onMeasureChange` reports one frame after observation starts, then when
94
+ the border-box `width` or `height` changes. `changed` contains all fields changed
95
+ since the last event; `current` is the full snapshot. Reporting pauses off screen
96
+ and resumes with a fresh snapshot when Box returns.
97
+
98
+ `observe` accepts one of the eight selections below, or a typed array combining
99
+ them. `observe={['size', 'edges']}` and `observe={['edges', 'size']}` select the
100
+ same triggers. Repeated selections have no effect. Changing the selected
101
+ measurement fields requests a fresh snapshot. TypeScript checks each value;
102
+ use an array to combine selections in JSX.
103
+
104
+ The selections below progress from resize and context signals to continuous
105
+ scroll tracking. Intensity describes typical observation work and event frequency,
106
+ not a fixed performance rating. Content churn and the work in your handler also
107
+ affect the cost.
108
+
109
+ | Selection | Event triggers | Frontend work and intensity |
110
+ | --- | --- | --- |
111
+ | `width` | Border-box `width`. | Low during ordinary use: observes the host; no content or scroll observers. Can report each frame during resizing. |
112
+ | `height` | Border-box `height`. | Same work as `width`, with height as the event trigger. |
113
+ | `size` | Either border-box `width` or `height`. | Same observers as a single dimension; observes the element itself. |
114
+ | `context` | Theme, resolved font, insets, background, focus, and browser/device context through `onContextChange`. | Usually infrequent: shared theme/media listeners and local style/focus reads; no scroll listener. |
115
+ | `overflow` | `clientWidth`, `clientHeight`, `scrollWidth`, `scrollHeight`, `overflowX/Y`, `clippedX/Y`, and `scrollableX/Y`. | Content-dependent: adds content mutations and child resizes; no scroll listener. Does not report individual child dimensions. |
116
+ | `edges` | `hiddenStartX`, `hiddenEndX`, `hiddenStartY`, and `hiddenEndY`. | Measures content/layout changes and scrolling, at most once per frame. Emits only when an edge flag changes, so moving through the middle stays silent. |
117
+ | `scroll` | `scrollLeft` or `scrollTop`. | High during scrolling: measures content/layout and scroll changes and can emit every frame. |
118
+ | `all` | Every measurement field and rendering context. | Enables all observers and can emit every frame. |
119
+
120
+ Selections are independent. `scroll` selects offset triggers; it does not include
121
+ `size`, `overflow`, `edges`, or `context`. Every measurement event still
122
+ contains fresh values for every measurement field, including client/content
123
+ dimensions and hidden edges. Combine triggers when you also need events caused
124
+ by those other changes. With `observe="size"`, content-only changes cause no
125
+ event; the next size event includes the current content measurements.
126
+
127
+ For a scroll-edge indicator, use `edges`: it reads during scrolling but
128
+ avoids a callback for each offset change. `scroll` and `all` can cause the most
129
+ frequent application updates, especially when the handler sets state or sends
130
+ each snapshot to a worker. Prefer `fade` when CSS alone needs the edge state.
131
+
132
+ When no measurement is selected, `onMeasureChange` adds `size`;
133
+ `onContextChange` adds `context`.
134
+ Passing both handlers observes size and context without enabling scroll events.
135
+ Without handlers or `fade`, omitting `observe` keeps Box idle.
136
+
137
+ Set `throttle` to a minimum interval in milliseconds. The first report has no
138
+ added delay; subsequent reports include a trailing update with the latest values.
139
+ Omit it or pass `0` for frame-based reporting. Negative or non-finite values use
140
+ `0`. The interval applies to `onMeasureChange`; context events are not throttled.
141
+ Throttling limits event delivery; it does not reduce observer reads.
142
+ `edges`, `scroll`, and `fade` continue measuring during scrolling so their
143
+ CSS states remain current.
144
+
145
+ `Tooltip truncatedOnly` reads Box's clipping on demand. It works without
146
+ `observe` or `onMeasureChange` and does not enable continuous observation.
147
+
148
+ This readout observes `['size', 'overflow', 'edges', 'scroll']` with `throttle={100}`.
149
+ Resize or scroll the Box to update it.
96
150
 
97
151
  ```tsx title="measurechange"
98
152
  const [measurement, setMeasurement] = useState<BoxMeasurement | null>(null)
@@ -100,6 +154,8 @@ const [measurement, setMeasurement] = useState<BoxMeasurement | null>(null)
100
154
  <Box
101
155
  round={8}
102
156
  className="measure-probe-box"
157
+ observe={['size', 'overflow', 'edges', 'scroll']}
158
+ throttle={100}
103
159
  onMeasureChange={(_, { current }) => setMeasurement(current)}
104
160
  >
105
161
  <Text size="small" priority="tertiary">Resize or scroll this Box.</Text>
@@ -224,14 +280,17 @@ const canvasRef = useRef<HTMLCanvasElement>(null)
224
280
 
225
281
  | Prop | Type | Default | Description |
226
282
  |------|------|---------|-------------|
227
- | `display?` | BoxDisplay | block | Layout model for the host. All other layout, sizing, mask, and shadow properties stay ordinary `className` / `style` CSS on the Box itself. |
283
+ | `display?` | 'block' \| 'inline-block' \| 'flex' \| 'inline-flex' \| 'grid' \| 'inline-grid' | block | Layout model for the host. Sizing, alignment, mask, and shadow properties stay ordinary `className` / `style` CSS on the Box itself. |
228
284
  | `fade?` | boolean | — | Fades out every edge that currently hides clipped content, and drops the fade from an edge once the reader scrolls to it. |
229
285
  | `fadeSize?` | number \| string | 24 | Depth of the `fade` gradient. A `number` is pixels; a string is any CSS length. |
230
286
  | `gap?` | number \| string | — | Gap between children, matching the CSS `gap` property. A `number` is pixels; a string is any CSS length or two-value gap (`'1rem'`, `'8px 16px'`). Applies while the Box is a flex or grid container. |
231
- | `observe?` | 'size' \| 'context' \| 'all' | — | What the Box watches, when a handler is not what turns it on. `'size'` keeps the overflow CSS states (`:state(clipped-x)`, `:state(scrollable-y)`, …) current — reach for it when your own CSS is the only reader. `'context'` and `'all'` are there for symmetry; passing `onMeasureChange` or `onContextChange` already turns the matching half on. |
287
+ | `margin?` | number \| string | — | Outer spacing, matching CSS `margin`. Numbers are pixels; strings accept CSS shorthand, `auto`, negative lengths, and custom properties. Omission adds no style. |
288
+ | `observe?` | 'width' \| 'height' \| 'size' \| 'context' \| 'overflow' \| 'edges' \| 'scroll' \| 'all' \| readonly BoxObservation[] | — | One selection or an array of selections, in any order. `'size'` watches width and height; `'context'` watches rendering context; `'overflow'` watches content dimensions and clipping. `'edges'` reports which edges hide content; `'scroll'` reports offsets, potentially every frame; `'all'` selects everything. Selections are independent: use `['size', 'edges']` to combine them. A measurement handler implies `'size'` when no measurement is selected; a context handler adds `'context'`. Size skips content and scroll observers; overflow adds content observation; hidden edges and scroll add scroll reads. Without handlers or `fade`, omission stays idle. |
232
289
  | `onContextChange?` | (event, detail) => void | — | Fired after Box's browser and local rendering context changes. `detail` contains the changed fields and a full current snapshot. |
233
- | `onMeasureChange?` | (event, detail) => void | — | Fired after Box geometry or its content-overflow state changes. `detail` contains the changed fields and a full current snapshot. |
290
+ | `onMeasureChange?` | (event, detail) => void | — | Fired when a selected measurement field changes. `detail` contains all fields changed since the last event and a full current snapshot. |
291
+ | `padding?` | number \| string | — | Inner spacing, matching CSS `padding`. Numbers are pixels; strings accept CSS shorthand, percentages, and custom properties. Omission adds no style. |
234
292
  | `round?` | boolean \| number \| string | — | Fully-round corners (`border-radius: 999px`, clamped to the box). Pass a `number` (px) or a CSS length string (`'1rem'`) for a custom radius. Omit for square corners. |
293
+ | `throttle?` | number | 0 | Minimum interval between measurement events, in milliseconds. The first report has no added delay; a trailing report delivers the latest values. Active observers and CSS clipping states are not throttled. |
235
294
 
236
295
  ### BoxMeasurement
237
296
 
@@ -261,21 +320,21 @@ const canvasRef = useRef<HTMLCanvasElement>(null)
261
320
  | Field | Type | Default | Description |
262
321
  |------|------|---------|-------------|
263
322
  | `backgroundColor` | string | — | Resolved `background-color`. Needed when the box's content is drawn somewhere else — an offscreen canvas, a worker, an export — where the box's own background is not behind it. |
264
- | `browser` | BoxBrowser | — | Browser family. |
323
+ | `browser` | 'chrome' \| 'edge' \| 'firefox' \| 'opera' \| 'safari' \| 'unknown' | — | Browser family. |
265
324
  | `browserVersion` | number | — | Browser major version, or `0` when unknown. Minor and patch digits are frozen by every engine, so only the major number is reported. |
266
325
  | `devicePixelRatio` | number | — | `window.devicePixelRatio`: CSS pixels per device pixel. `1` on a standard display, `2` on most Retina screens, and a fraction under OS or browser zoom. Live — it re-reports on zoom and when the window moves to a monitor with a different density. |
267
326
  | `focusWithin` | boolean | — | Whether focus is on the box or any of its descendants, read from the native `:focus-within`. For CSS, use that pseudo-class directly; this field is for logic that cannot query the DOM. |
268
327
  | `font` | BoxFont | — | Resolved text style, ready to hand to a canvas 2D context. |
269
- | `globalMode` | BoxMode | — | Mode on `<html>`, independent of an enclosing local scope. |
328
+ | `globalMode` | 'light' \| 'dark' | — | Mode on `<html>`, independent of an enclosing local scope. |
270
329
  | `hover` | boolean | — | Whether ordinary hover interaction is available. |
271
330
  | `inset` | BoxInset | — | Padding and border widths, for placing content inside the border box. |
272
331
  | `mobile` | boolean | — | Whether the browser reports a mobile device. |
273
- | `mode` | BoxMode | — | Closest scoped Anta mode. A local `.light` can override a dark document. |
274
- | `os` | BoxOS | — | Operating-system family. |
332
+ | `mode` | 'light' \| 'dark' | — | Closest scoped Anta mode. A local `.light` can override a dark document. |
333
+ | `os` | 'android' \| 'ios' \| 'linux' \| 'macos' \| 'windows' \| 'unknown' | — | Operating-system family. |
275
334
  | `osVersion` | number | — | Operating-system major version, or `0` when the browser withholds it. Browsers freeze this: every engine reports macOS as `10.15.7` and Windows 11 as `10.0`, so only Android and iOS carry a real number. Treat it as a hint, never as a gate. |
276
- | `pointer` | BoxPointer | — | Most precise available primary pointer. |
335
+ | `pointer` | 'fine' \| 'coarse' \| 'none' | — | Most precise available primary pointer. |
277
336
  | `reducedMotion` | boolean | — | Whether the reader asks for reduced motion. |
278
- | `systemAppearance` | BoxMode | — | Browser / operating-system color preference, independent of Anta classes. |
337
+ | `systemAppearance` | 'light' \| 'dark' | — | Browser / operating-system color preference, independent of Anta classes. |
279
338
 
280
339
  ### BoxFont
281
340
 
@@ -318,8 +377,11 @@ const canvasRef = useRef<HTMLCanvasElement>(null)
318
377
  ## Web component
319
378
 
320
379
  Use `<a-box>` without JSX. Events are non-bubbling `CustomEvent`s with the same
321
- `detail`. Set `observe="size"`, `"context"`, or `"all"` explicitly; adding a
322
- listener does not enable observation. A bare `observe` means `"all"`.
380
+ `detail`. Set `observe` explicitly; adding a listener does not enable
381
+ observation. Combine the eight selections with spaces, such as
382
+ `observe="size edges"` or `observe="size scroll"`, in any order. Repeated tokens
383
+ have no effect; unknown tokens are ignored. A bare `observe` means `"all"`.
384
+ `throttle="100"` limits measurement events to a 100 ms interval.
323
385
 
324
386
  ```html title="a-box"
325
387
  <a-box display="grid" gap="8px" round="12px" observe="all"
@@ -335,20 +397,22 @@ Import `@antadesign/anta/elements/a-box` to register the element. Listen for
335
397
 
336
398
  ## Styling
337
399
 
338
- Your `className` and `style` override `display`, `gap`, and `round` through the
400
+ Your `className` and `style` override `display`, `gap`, `padding`, `margin`, and
401
+ `round` through the
339
402
  `anta.components` CSS layer.
340
403
 
341
- Without typed CSS `attr()` support, raw `gap`, `round`, and `fade-size` length
342
- attributes need matching custom properties. The JSX wrapper sets these for you:
404
+ Without typed CSS `attr()` support, raw `gap`, `padding`, `margin`, `round`,
405
+ and `fade-size` attributes need matching custom properties. The JSX wrapper
406
+ sets these for you. Raw HTML lengths need units, such as `padding="16px"`:
343
407
 
344
408
  ```html
345
- <a-box display="flex" gap round fade class="raw-box"
346
- style="--box-gap: 8px; --box-round: 12px; --box-fade-size: 32px">
409
+ <a-box display="flex" gap padding margin round fade class="raw-box"
410
+ style="--box-gap: 8px; --box-padding: 12px; --box-margin: 0 auto; --box-round: 12px; --box-fade-size: 32px">
347
411
  <span>Layout</span><span>Context</span><span>Measurements</span>
348
412
  </a-box>
349
413
  ```
350
414
 
351
415
  ```css
352
- .raw-box { width: 190px; overflow: auto; padding: 12px; border: 1px solid var(--border-4); }
416
+ .raw-box { width: 190px; overflow: auto; border: 1px solid var(--border-4); }
353
417
  .raw-box span { flex: 0 0 auto; }
354
418
  ```
@@ -4,14 +4,46 @@ Hierarchy navigation assembled from Anta Buttons. Breadcrumb entries can be
4
4
  links, actions, or copy controls. Set `maxItems` to fold one middle range into a
5
5
  More menu without measuring the available width.
6
6
 
7
+ Breadcrumbs do not scroll horizontally by default. Use `maxItems` to limit
8
+ visible entries, or enable scrolling with the [scrolling recipe](#horizontal-scrolling).
9
+
7
10
  ## Items
8
11
 
9
- Pass ordered data through `items`. An `href` renders a native link with the
10
- Button appearance; omit it for an action button. Each item accepts `label`,
11
- `icon`, `iconTrailing`, `tone`, `disabled`, `onClick`, `className`, `style`,
12
- `id`, `title`, and `data-*` / `aria-*` attributes. `size`, priority, and
13
- underline treatment belong to the whole trail so every visible control retains
14
- the same height.
12
+ Pass ordered `BreadcrumbItem` objects through `items`. Each item is a link,
13
+ action, or copy control. `size`, `priority`, and underline treatment belong to
14
+ the whole trail.
15
+
16
+ ### BreadcrumbItem reference
17
+
18
+ | Property | Possible values | Description |
19
+ | --- | --- | --- |
20
+ | `label` | `React.ReactNode` | Required content, such as text, an image, or JSX. Also used as the label when the item folds into More. |
21
+ | `icon` | `IconShape` | Leading Anta icon shape. |
22
+ | `iconTrailing` | `IconShape` | Trailing Anta icon shape. Available on link and action items. |
23
+ | `tone` | `'neutral'`, `'brand'`, `'info'`, `'success'`, `'warning'`, `'critical'`, or a CSS color string | Item color. Defaults to neutral. |
24
+ | `disabled` | `boolean` | Disables the item. |
25
+ | `current` | `boolean` | Sets `aria-current="page"`. Does not change the tone. |
26
+ | `onClick` | `(event: any) => void` | Activation handler. Folded items call it on menu selection. |
27
+ | `href` | `string` | Destination URL for a link item. |
28
+ | `target` | `string` | Link target, such as `'_blank'`. Requires `href`. |
29
+ | `copy` | `string` | Literal text to copy. |
30
+ | `copyNode` | `boolean` or `string` | `true` copies the nearest ancestor marked `data-copy-source`; a CSS selector chooses an ancestor region. `false` disables the node target. |
31
+ | `copyUrl` | `true` | Copies the current page URL. |
32
+ | `copyWithUrl` | `boolean` | Prefixes literal `copy` text with the current page URL. |
33
+ | `onCopyRequest` | `() => void` | Refreshes a literal `copy` value before activation. Update application state; return values are ignored. |
34
+ | `onCopied` | `(ok: boolean) => void` | Reports whether a copy attempt succeeded. Available on copy items. |
35
+ | `iconPlacement` | `'leading'`, `'trailing'`, or `'none'` | Copy glyph placement. Defaults to `'leading'`. |
36
+ | `copiedLabel` | `string` | Confirmation text for a copy item with no icon. Defaults to `'Copied'`. |
37
+ | `className` | `string` | CSS class on the item control. |
38
+ | `style` | `React.CSSProperties` | Inline styles on the item control. |
39
+ | `id` | `string` | HTML ID on the item control. |
40
+ | `title` | `string` | Native browser tooltip text. |
41
+ | `data-*`, `aria-*` | Attribute-specific values | Attributes forwarded to the item control. |
42
+
43
+ Use `href` for a link, omit navigation and copy props for an action, or choose
44
+ exactly one of `copy`, `copyNode`, and `copyUrl` for a copy item. Copy items
45
+ cannot use `href`, `target`, or `iconTrailing`. `copyWithUrl` and
46
+ `onCopyRequest` apply only to literal `copy` text.
15
47
 
16
48
  ```tsx
17
49
  <Breadcrumbs
@@ -167,12 +199,49 @@ Use `priority`, `size`, and `separator` before adding CSS. A `className` on
167
199
  their individual Buttons. The composed structure uses light-DOM tags, so plain
168
200
  CSS can style a separator directly.
169
201
 
202
+ To tighten the space between items, give the trail a class and set
203
+ `a-breadcrumb-separator` to `width: 8px` within that class. This changes the
204
+ separator's layout width while keeping the icon size unchanged. The website
205
+ header uses this approach. `project-breadcrumbs` is a demo class; replace it
206
+ with your own selector.
207
+
170
208
  ```tsx
171
209
  <Breadcrumbs className="project-breadcrumbs" separator="chevron-right" items={items} />
172
210
  ```
173
211
 
174
212
  ```css
175
213
  .project-breadcrumbs a-breadcrumb-separator {
214
+ width: 8px;
176
215
  color: var(--text-2);
177
216
  }
178
217
  ```
218
+
219
+ ### Horizontal scrolling
220
+
221
+ Set `overflow-x: auto` on the trail to scroll when its contents exceed the
222
+ available width. `maxItems` limits the item count; long labels can still overflow.
223
+
224
+ Horizontal scrolling also makes vertical overflow compute to `auto`, which
225
+ clips the default button focus ring above and below the row. Set
226
+ `outline-offset: -1px` on the trail's focused controls to draw the ring inside
227
+ their bounds. The selector below covers breadcrumb links, action and copy
228
+ buttons, and the More control.
229
+
230
+ `scrolling-breadcrumbs` is an example class; replace it with your own selector.
231
+ Use Tab to move through the controls and see the focus ring.
232
+
233
+ ```tsx
234
+ <Box style={{ width: 280, maxWidth: '100%' }}>
235
+ <Breadcrumbs className="scrolling-breadcrumbs" maxItems={3} items={[
236
+ { label: 'Anta', href: '/' },
237
+ { label: 'Documentation', href: '/box/' },
238
+ { label: 'Layout components', href: '/panel/' },
239
+ { label: 'Breadcrumbs', current: true },
240
+ ]} />
241
+ </Box>
242
+ ```
243
+
244
+ ```css
245
+ .scrolling-breadcrumbs { overflow-x: auto; }
246
+ .scrolling-breadcrumbs > :focus-visible { outline-offset: -1px; }
247
+ ```
@@ -166,8 +166,8 @@ name "trash". Pass your own `aria-label` to override.
166
166
  ### Children
167
167
 
168
168
  ```tsx
169
- <Button tone="brand" label="Save" icon="check">
170
- <span style={{ opacity: 0.7 }}>⌘S</span>
169
+ <Button tone="brand" label="Save" icon="check" aria-keyshortcuts="Meta+s">
170
+ <kbd aria-hidden="true" style={{ font: 'inherit', fontVariationSettings: 'inherit', opacity: 0.7 }}>⌘S</kbd>
171
171
  </Button>
172
172
  ```
173
173
 
@@ -214,9 +214,9 @@ native text selection or touch gestures.
214
214
  | `onWheelInput?` | (event, detail) => void | — | Accepted wheel input, with a serialized original event, Capture-relative geometry, focus state, and activation reason. Cancellation is already complete. |
215
215
  | `pan?` | boolean \| CapturePan | — | Emit custom pan motion. `true` enables touch panning on both axes without momentum. Options select devices, axes, bounds directions, and optional inertia. Captures the pointer internally; `pointerCapture` is not required. Sets CSS touch-action through attributes before the gesture starts. |
216
216
  | `pointerCapture?` | boolean \| CapturePointerCapture | — | Emit raw data for a primary pointer until release or cancellation. An options object filters devices/buttons and configures activation. Nested interactive controls are excluded unless explicitly included. A listener alone enables nothing. |
217
- | `wheelActivation?` | CaptureWheelActivation | settled | Pointer or focus condition required before wheel input can be captured. Focus applies only to input targeted within this Capture. |
217
+ | `wheelActivation?` | 'hover' \| 'settled' \| 'focus' \| 'settled-or-focus' | settled | Pointer or focus condition required before wheel input can be captured. Focus applies only to input targeted within this Capture. |
218
218
  | `wheelCapture?` | CaptureInputDirections | — | Capture wheel input in the enabled directions and emit `onWheelInput`. `true` accepts all directions. Omit or pass `false` to leave wheel input alone. Nested native wheel controls and Anta menus are excluded. All-false direction bounds preserve pointer settling while declining input. A listener alone never enables capture. |
219
- | `wheelModifier?` | CaptureInputModifier | none | Required modifier for wheel capture. `none` preserves browser Ctrl/pinch zoom. |
219
+ | `wheelModifier?` | 'none' \| 'any' \| 'alt' \| 'ctrl' \| 'meta' \| 'shift' | none | Required modifier for wheel capture. `none` preserves browser Ctrl/pinch zoom. |
220
220
  | `wheelSettle?` | CaptureWheelSettle | { delay: 150, tolerance: 5, resetOnMove: false } | Dwell delay, movement tolerance, and whether movement resets eligibility. |
221
221
 
222
222
  ### CaptureWheelSettle
@@ -233,7 +233,7 @@ native text selection or touch gestures.
233
233
  |------|------|---------|-------------|
234
234
  | `buttons?` | readonly number[] | [0] | Accepted initiating buttons, using PointerEvent.button values. |
235
235
  | `includeInteractive?` | boolean | false | Allow capture to start on nested native or ARIA controls, links, or editable regions. |
236
- | `modifier?` | CaptureInputModifier | any | Modifier required to start a capture session. |
236
+ | `modifier?` | 'none' \| 'any' \| 'alt' \| 'ctrl' \| 'meta' \| 'shift' | any | Modifier required to start a capture session. |
237
237
  | `pointerTypes?` | readonly CapturePointerType[] | ['mouse', 'pen', 'touch'] | Accepted pointer devices. One primary pointer is tracked per Capture. |
238
238
  | `threshold?` | number | 0 | Movement required before activation, in viewport CSS pixels. |
239
239
 
@@ -258,7 +258,7 @@ native text selection or touch gestures.
258
258
 
259
259
  | Field | Type | Default | Description |
260
260
  |------|------|---------|-------------|
261
- | `activationReason` | CaptureWheelActivationReason | — | |
261
+ | `activationReason` | 'immediate' \| 'settled' \| 'focus' | — | |
262
262
  | `boxHeight` | number | — | |
263
263
  | `boxWidth` | number | — | Dimensions of the same viewport bounding rectangle, in CSS pixels. |
264
264
  | `focusWithin` | boolean | — | |
@@ -271,7 +271,7 @@ native text selection or touch gestures.
271
271
 
272
272
  | Field | Type | Default | Description |
273
273
  |------|------|---------|-------------|
274
- | `activationReason` | CapturePointerActivationReason | — | |
274
+ | `activationReason` | 'pointer-down' \| 'drag-threshold' | — | |
275
275
  | `boxHeight` | number | — | |
276
276
  | `boxWidth` | number | — | Dimensions of the same viewport bounding rectangle, in CSS pixels. |
277
277
  | `deltaX` | number | — | Movement since the previous delivered sample, in viewport CSS pixels. |
@@ -285,13 +285,13 @@ native text selection or touch gestures.
285
285
  | `phase` | 'start' \| 'move' \| 'end' \| 'cancel' | — | |
286
286
  | `pointerEvent` | SerializedPointerEvent \| null | — | Null when cancellation comes from lifecycle or configuration changes. |
287
287
  | `start` | CapturePointerStart | — | |
288
- | `cancelReason?` | CaptureInputCancelReason | — | |
288
+ | `cancelReason?` | 'pointer-cancel' \| 'lost-capture' \| 'disabled' \| 'disconnected' \| 'blur' \| 'interrupted' | — | |
289
289
 
290
290
  ### CapturePanInput
291
291
 
292
292
  | Field | Type | Default | Description |
293
293
  |------|------|---------|-------------|
294
- | `activationReason` | CapturePointerActivationReason | — | |
294
+ | `activationReason` | 'pointer-down' \| 'drag-threshold' | — | |
295
295
  | `boxHeight` | number | — | |
296
296
  | `boxWidth` | number | — | Dimensions of the same viewport bounding rectangle, in CSS pixels. |
297
297
  | `deltaX` | number | — | Incremental scroll motion in viewport CSS pixels. |
@@ -305,7 +305,7 @@ native text selection or touch gestures.
305
305
  | `start` | CapturePointerStart | — | |
306
306
  | `velocityX` | number | — | Scroll velocity in viewport CSS pixels/ms. |
307
307
  | `velocityY` | number | — | |
308
- | `cancelReason?` | CaptureInputCancelReason | — | |
308
+ | `cancelReason?` | 'pointer-cancel' \| 'lost-capture' \| 'disabled' \| 'disconnected' \| 'blur' \| 'interrupted' | — | |
309
309
 
310
310
  ## Web component
311
311
 
@@ -20,20 +20,20 @@ neutral. Six named tones (`neutral` default), or pass any CSS color for a custom
20
20
 
21
21
  ## Selected-only tone
22
22
 
23
- `toneSelected` tones the **checked mark only** — the empty box stays neutral grey
24
- until it's checked. Reach for it instead of `tone` when a resting tinted border would
25
- read as a validation error. Same value set as `tone`.
23
+ `toneScope="selected"` applies the checkbox's `tone` to the **checked mark only** —
24
+ the empty box stays neutral grey until it's checked. Use it when a resting tinted
25
+ border would read as a validation error.
26
26
 
27
27
  ```tsx
28
- <Checkbox toneSelected="brand">Off stays neutral</Checkbox>
29
- <Checkbox defaultChecked toneSelected="critical">Checked mark tinted</Checkbox>
28
+ <Checkbox tone="brand" toneScope="selected">Off stays neutral</Checkbox>
29
+ <Checkbox defaultChecked tone="critical" toneScope="selected">Checked mark tinted</Checkbox>
30
30
  ```
31
31
 
32
32
  ## Coloring the text
33
33
 
34
34
  There's no text-tone prop. To tint the **label**, set `color` on the checkbox with a
35
35
  theme-aware `--text-N-{tone}` token (the label inherits it); for the **hint**, add a
36
- `color` rule on the `a-checkbox-hint` child. Pair either with `tone` / `toneSelected`
36
+ `color` rule on the `a-checkbox-hint` child. Pair either with `tone`
37
37
  to tint the whole control.
38
38
 
39
39
  ```css
@@ -163,7 +163,11 @@ function ParentChild() {
163
163
 
164
164
  ## Accessibility
165
165
 
166
- The wrapper sets `role="checkbox"` and `aria-checked` (`"true"` / `"false"` / `"mixed"`), and derives the accessible name from `label` or `children`. For a label-less checkbox, pass `aria-label`.
166
+ The wrapper sets `role="checkbox"`, and the element keeps `aria-checked`
167
+ (`"true"` / `"false"` / `"mixed"`) in sync. Its light-DOM label supplies the
168
+ accessible name and its hint supplies the accessible description through
169
+ `ElementInternals`; no generated IDs are needed. For a label-less checkbox, pass
170
+ `aria-label`.
167
171
 
168
172
  Space toggles the checkbox. Follows the [WAI-ARIA checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/).
169
173
 
@@ -174,7 +178,7 @@ Space toggles the checkbox. Follows the [WAI-ARIA checkbox pattern](https://www.
174
178
  | `checked?` | CheckboxValue | — | Controlled checked state. When provided the checkbox is controlled — it renders exactly this and never self-applies; `onStateChange` is a *request* the consumer accepts by updating this prop. Use `defaultChecked` for an uncontrolled checkbox. `'indeterminate'` shows the minus glyph and takes visual precedence; clicking it requests `true`. |
175
179
  | `defaultChecked?` | CheckboxValue | false | Initial checked state for an uncontrolled checkbox. Read once; later changes are ignored and the element updates its state after interaction. |
176
180
  | `disabled?` | boolean | — | Disable the checkbox (no interaction, dropped from the tab order). |
177
- | `hint?` | ReactNode | — | Secondary text rendered under the label — explanatory copy, like Input's hint. Not part of the accessible name. |
181
+ | `hint?` | ReactNode | — | Secondary text rendered under the label and exposed as the checkbox's accessible description. Not part of the accessible name. |
178
182
  | `label?` | string | — | Visible label — the *value* of the checkbox (clicked along with the box). Convenience for the common single-string case; for richer content (markup, a link, an info icon) use `children`. When both are supplied, `label` renders first. Required unless `children` or `aria-label` is provided (a `role="checkbox"` takes its name from the author, not the markup). |
179
183
  | `name?` | string | — | Form field name. Inside a `<form>` the checkbox submits under this name, contributing `value` when checked — like a native checkbox. |
180
184
  | `onChange?` | (event) => void | — | Fired *after* the checked state changes — a native `change` event (the post-apply counterpart to `onStateChange`). Not cancelable. For a controlled checkbox this fires once you've updated `checked`. |
@@ -182,15 +186,16 @@ Space toggles the checkbox. Follows the [WAI-ARIA checkbox pattern](https://www.
182
186
  | `onValueChange?` | (event, attrs) => void | — | Like `onChange`, but with a `{ checked, indeterminate, name, value }` snapshot as the second argument, matching `Input`'s `onValueChange`. |
183
187
  | `round?` | boolean \| number \| string | — | Round the checkbox mark to a circle (`border-radius: 999px` on the box). Pass a `number` (px) or a CSS length string for a rounded-square mark instead. |
184
188
  | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size variant. small=14px, medium=16px, large=18px box. |
185
- | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Color of the **mark** in every state — the checked-box fill *and* the unselected box border. A named tone or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone. Named tones track light/dark mode automatically; a custom color keeps its hue + chroma and pins lightness to the fill curve. Use `toneSelected` instead to tone only the checked mark and leave the empty box neutral. The label + hint stay neutral — recolor them in plain CSS via the theme-aware `--text-N-{tone}` tokens. |
186
- | `toneSelected?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Like `tone`, but colored onto the **checked mark only** — the empty box stays neutral grey until it's checked. Prefer this over `tone` when a resting tinted border would read as a validation state. Same value set as `tone`; if both are set, `tone` governs the off-state border and `toneSelected` the checked fill. |
189
+ | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Color of the **mark**. In the default `all` scope this colors the checked-box fill and unselected box border. A named tone or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone. Named tones track light/dark mode automatically; a custom color keeps its hue + chroma and pins lightness to the fill curve. Set `toneScope="selected"` to tone only the checked mark and leave the empty box neutral. The label + hint stay neutral — recolor them in plain CSS via the theme-aware `--text-N-{tone}` tokens. |
190
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply `tone` to every state, or only while checked so the empty box stays neutral. `selected` is useful when a tinted resting border would read as a validation state. |
187
191
  | `value?` | string | "on" | Value submitted with the form when checked — like a native checkbox. |
188
192
 
189
193
  ## Web Component
190
194
 
191
195
  Use the web component directly when you are not using React or Preact and a native control does not fit.
192
196
 
193
- The focusable host carries the state. Label and hint are light-DOM children.
197
+ The focusable host carries the state. Label and hint are light-DOM children that
198
+ the element exposes as its accessible name and description.
194
199
 
195
200
  ```html
196
201
  <a-checkbox role="checkbox" tabindex="0" name="updates" default-state="checked">
@@ -205,22 +210,22 @@ For a standard HTML form, add `data-anta` to a native checkbox. It keeps the
205
210
  browser's label behavior, checked and indeterminate states, and form submission
206
211
  while using Anta's control surface.
207
212
 
208
- `size`, `tone`, `tone-selected`, and `round` use the matching Checkbox visual
213
+ `size`, `tone`, `tone-scope`, and `round` use the matching Checkbox visual
209
214
  treatments.
210
215
 
211
216
  ```html
212
217
  <div style="display: grid; gap: 8px">
213
218
  <label><input data-anta type="checkbox" name="updates" size="small" tone="brand"> Product updates</label>
214
- <label><input data-anta type="checkbox" name="security" tone-selected="success" checked> Security notices</label>
219
+ <label><input data-anta type="checkbox" name="security" tone="success" tone-scope="selected" checked> Security notices</label>
215
220
  <label><input data-anta type="checkbox" name="billing" size="large" round tone="warning" checked> Billing alerts</label>
216
221
  </div>
217
222
  ```
218
223
 
219
224
  ## Styling
220
225
 
221
- Reach for the props first: **`tone`** colors the mark in every state,
222
- **`toneSelected`** only the checked mark (any CSS color for a custom tone — it
223
- derives the full rest/hover/active curve in oklch), **`size`** the dimensions + type.
226
+ Reach for the props first: **`tone`** chooses the mark color and **`toneScope`**
227
+ chooses whether it applies in `all` states or only while `selected` (any CSS color
228
+ derives the full rest/hover/active curve in oklch); **`size`** controls dimensions + type.
224
229
  To tint the label + hint, add a `color` rule (on the host for the label, on
225
230
  `a-checkbox-hint` for the hint) with the `--text-N-{tone}` tokens — there's no
226
231
  text-tone prop. The focus ring is the global
@@ -23,6 +23,12 @@ the first suggestion (then ↑/↓ move through them), and
23
23
  Enter on a highlighted row picks it. Esc or an outside click
24
24
  closes the list; clicking the field to place the caret keeps it open.
25
25
 
26
+ The focused field is associated directly with the suggestion popup and its
27
+ active option through ARIA element reflection. These relationships do not
28
+ require generated popup or option IDs, so separate application roots cannot
29
+ collide. Explicit ARIA relationships authored on a raw Anta Input take
30
+ precedence.
31
+
26
32
  ## Filtering
27
33
 
28
34
  `filter` decides how suggestions match the text:
@@ -212,9 +212,15 @@ the standard constraint-validation pipeline.
212
212
 
213
213
  ## Accessibility
214
214
 
215
+ The focused Input field is associated directly with its Calendar dialog through
216
+ ARIA element reflection. The relationship does not require a generated popup ID,
217
+ so separate application roots can render InputDate instances without ID
218
+ collisions. An explicitly authored relationship on a raw Anta Input takes
219
+ precedence.
220
+
215
221
  The calendar is a labelled **group** of day buttons, a flat grid rather than a
216
- nested `grid`/`row`/`gridcell` tree. The group is named by the visible month
217
- (`aria-labelledby`). Each day is a `<button>` whose `aria-label` is its full date
222
+ nested `grid`/`row`/`gridcell` tree. The visible month names the group directly.
223
+ Each day is a `<button>` whose `aria-label` is its full date
218
224
  including weekday ("Sunday, June 28, 2026"), with `aria-pressed` on the selected day
219
225
  and `aria-current="date"` on today. The decorative weekday headers are `aria-hidden`,
220
226
  since each day already names its weekday. A roving `tabindex` keeps a single tab