@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,182 @@
1
+ // The demo source lives in a sibling `.ts` file rather than inline
2
+ // here because Astro's MDX pipeline trims leading whitespace from
3
+ // JSX-attribute template literals, which scrambles the indentation
4
+ // Monaco needs for folding ranges. A regular module import preserves
5
+ // the template literal's bytes verbatim.
6
+
7
+ # Progress
8
+
9
+ `Progress` shows task completion. Pass `value` for known progress. Omit it, or
10
+ pass `false`, while work is underway without a known duration.
11
+
12
+ ## Value, label and hint
13
+
14
+ ```tsx
15
+ <Progress value={60} />
16
+ <Progress value={42} label="Uploading files…" hint="3 of 7" />
17
+
18
+ // Omit value, or pass false, while the duration is unknown.
19
+ <Progress label="Preparing upload…" hint="~ 3 minutes left" />
20
+ ```
21
+
22
+ `value` sets known completion relative to `max`, which defaults to `100`.
23
+ `label` follows the percentage. `hint` is right-aligned. With no `value`, the
24
+ label identifies indeterminate work and the loading animation takes the track.
25
+
26
+ ## Size
27
+
28
+ ```tsx
29
+ <Progress size="small" tone="info" value={60} label="Small" hint="~ 3 minutes left" />
30
+ <Progress tone="info" value={60} label="Medium" hint="~ 3 minutes left" />
31
+ <Progress size="large" tone="info" value={60} label="Large" hint="~ 3 minutes left" />
32
+ ```
33
+
34
+ `size` scales the track and the default label row together. `medium` is the
35
+ default.
36
+
37
+ ## Tone
38
+
39
+ ```tsx
40
+ <Progress value={60} tone="brand" label="Brand" />
41
+ <Progress value={60} tone="info" label="Info" />
42
+ <Progress value={60} tone="success" label="Success" />
43
+ <Progress value={60} tone="warning" label="Warning" />
44
+ <Progress value={60} tone="critical" label="Critical" />
45
+ <Progress value={60} tone="#e0457b" label="Custom" />
46
+ ```
47
+
48
+ `tone` colors the track, indicator, and label row. Use `neutral` for the
49
+ default, a named semantic tone, or any CSS color.
50
+
51
+ ## Border
52
+
53
+ ```tsx
54
+ <Progress
55
+ value={60}
56
+ tone="warning"
57
+ label="Border on every edge"
58
+ hint="3 of 5"
59
+ style={{ borderWidth: '2px' }}
60
+ />
61
+
62
+ // A bottom border works when the indicator is at the top of the app.
63
+ <Progress
64
+ value={60}
65
+ tone="info"
66
+ label="Top-mounted indicator"
67
+ hint="3 of 5"
68
+ style={{ borderWidth: '0 0 1px' }}
69
+ />
70
+
71
+ // A top border works when the indicator is at the bottom of the app.
72
+ <Progress
73
+ value={60}
74
+ tone="brand"
75
+ label="Bottom-mounted indicator"
76
+ hint="3 of 5"
77
+ style={{ borderTopWidth: '1px' }}
78
+ />
79
+ ```
80
+
81
+ The track starts borderless. Set a border width to reveal its tone-aware border
82
+ color. A bottom edge works for an indicator at the top of an app. A top edge
83
+ works for one at the bottom.
84
+
85
+ ## Round
86
+
87
+ ```tsx
88
+ <Progress value={60} round label="Fully round" />
89
+ <Progress value={60} round={4} label="4px radius" />
90
+ ```
91
+
92
+ `round` makes the track fully round. Pass a number or CSS length for a custom
93
+ radius.
94
+
95
+ ### Props
96
+
97
+ | Prop | Type | Default | Description |
98
+ |------|------|---------|-------------|
99
+ | `hint?` | string | — | Right-aligned hint text (e.g. "3 of 7"). Like `label`, it's not rendered
100
+ when custom `children` are provided but still feeds the accessible name. |
101
+ | `label?` | string | — | Text label displayed after the percentage, or on its own for
102
+ indeterminate progress. When you provide custom `children` (which replace
103
+ the default label row), `label` is no longer rendered — but it still
104
+ supplies the progressbar's accessible name. |
105
+ | `max?` | number | 100 | Upper bound of the range. |
106
+ | `round?` | boolean \| number \| string | — | Fully-round track (`border-radius: 999px`); the fill is clipped to it. Pass a
107
+ `number` (px) or a CSS length string for a custom radius. |
108
+ | `size?` | 'small' \| 'medium' \| 'large' | medium | Size variant. Scales the track and the default label row together. |
109
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Color variant, or any literal CSS color for a one-off custom tone (the
110
+ surface / indicator / text are derived from it in oklch). Named tones track
111
+ light/dark automatically. |
112
+ | `value?` | number \| false | — | Current progress value. Omit this prop, or pass `false`, to show
113
+ indeterminate progress. Negative values are clamped to 0. |
114
+
115
+ ```html
116
+ <a-progress style="width: 100%" value="42" max="100" tone="info" role="progressbar"
117
+ aria-valuenow="42" aria-valuemin="0" aria-valuemax="100" aria-label="Upload, 42%">
118
+ <a-progress-label>
119
+ <a-progress-number>42%</a-progress-number>
120
+ <a-progress-text>Upload</a-progress-text>
121
+ </a-progress-label>
122
+ </a-progress>
123
+
124
+ <!-- Omit value for indeterminate progress. -->
125
+ <a-progress style="width: 100%" round="50px" role="progressbar" aria-label="Preparing upload">
126
+ <a-progress-label>
127
+ <a-progress-text>Preparing upload</a-progress-text>
128
+ </a-progress-label>
129
+ </a-progress>
130
+ ```
131
+
132
+ Use this when you are not using the React or Preact wrapper and a native HTML
133
+ control does not fit: construct the equivalent Anta web component from the
134
+ elements below.
135
+
136
+ Add the progressbar semantics and label structure when using the element
137
+ directly.
138
+
139
+ ### Native HTML progress
140
+
141
+ ```html
142
+ <progress
143
+ data-anta
144
+ value="42"
145
+ max="100"
146
+ tone="info"
147
+ aria-label="Uploading, 42%"
148
+ style="width: 100%"
149
+ ></progress>
150
+
151
+ <!-- Omit value for native indeterminate progress. -->
152
+ <progress data-anta round="50px" aria-label="Preparing upload" style="width: 100%"></progress>
153
+ ```
154
+
155
+ For a plain HTML progress indicator, add `data-anta` to `<progress>`. It keeps
156
+ the browser's native semantics, including indeterminate progress when `value`
157
+ is absent, while using Anta's track, fill, tone, and size treatments. Native
158
+ progress does not render Anta's label or hint structure; give it an accessible
159
+ name instead.
160
+
161
+ ```css
162
+ /* a fancy bar: rounded track (all sides), a multi-stop gradient fill with a glow,
163
+ and a crisp hard right edge (the default soft right-edge fade is cleared). */
164
+ a-progress.fancy {
165
+ border-radius: 999px;
166
+ background: light-dark(var(--bg-5), #26232c);
167
+ }
168
+ a-progress.fancy::part(indicator) {
169
+ background: linear-gradient(90deg, #6a5acd, #d44ea3 55%, #ff8a5b);
170
+ box-shadow: 0 0 12px rgba(212, 78, 163, 0.55); /* glow */
171
+ }
172
+ a-progress.fancy::part(indicator)::after { background: none; } /* hard right edge */
173
+
174
+ /* the percentage / label / hint are light-DOM children — recolor for the bar */
175
+ a-progress.fancy a-progress-number,
176
+ a-progress.fancy a-progress-text { color: #fff; }
177
+ a-progress.fancy a-progress-hint { color: var(--text-2); }
178
+ ```
179
+
180
+ For one-offs, the **track** is the host (style `a-progress` directly) and the
181
+ **indicator** (fill bar) is a shadow **part** — `::part(indicator)`. The `.fancy`
182
+ class is just for the demo.
@@ -0,0 +1,368 @@
1
+ # Radio
2
+
3
+ A single-select control. `RadioGroup` is the whole component — you pass it an
4
+ `options` array and it renders one radio per entry, owning selection, keyboard
5
+ navigation, and the form value. There's no separate `Radio` component: options
6
+ are data, not markup.
7
+
8
+ ## Options, label, and hint
9
+
10
+ ```tsx
11
+ <RadioGroup
12
+ label="How should we reach you?"
13
+ hint="We'll only use this for account alerts."
14
+ defaultValue="email"
15
+ options={[
16
+ { value: 'email', label: 'Email', hint: 'A confirmation link goes to your inbox.' },
17
+ { value: 'sms', label: 'SMS', hint: 'Standard message rates apply.' },
18
+ { value: 'push', label: 'Push notification' },
19
+ ]}
20
+ />
21
+ ```
22
+
23
+ Each entry needs a unique `value` (its identity and submitted value) and a
24
+ `label`, plus an optional `hint` — secondary copy rendered under that option,
25
+ styled like `Input`'s hint.
26
+
27
+ The group's own `label` and `hint` form a header **above** the options: the
28
+ `label` first, the `hint` (an instruction for the whole set) directly under it.
29
+
30
+ ## Tones
31
+
32
+ ```tsx
33
+ {/* Usually one tone for the whole group: */}
34
+ <RadioGroup
35
+ tone="info"
36
+ defaultValue="a"
37
+ options={[
38
+ { value: 'a', label: 'Option A' },
39
+ { value: 'b', label: 'Option B' },
40
+ ]}
41
+ />
42
+ ```
43
+
44
+ Six named tones — `neutral` (default), `brand`, `info`, `success`, `warning`,
45
+ `critical`. `tone` colors the **mark** in every state — the selected-ring fill + dot
46
+ and (for a tinted tone) the unselected ring border (a light shade of the fill that
47
+ darkens on hover and active; `neutral` keeps a grey ring). The label and hint stay
48
+ neutral — color them in plain CSS with the `--text-N-{tone}` tokens. Set `tone` on
49
+ the `RadioGroup` to color every option, or on a single option to override just that
50
+ one. Any literal CSS color works too, for a one-off custom tone.
51
+
52
+ ## Selected-only tone
53
+
54
+ ```tsx
55
+ <RadioGroup
56
+ toneSelected="brand"
57
+ defaultValue="a"
58
+ options={[
59
+ { value: 'a', label: 'Option A' },
60
+ { value: 'b', label: 'Option B' },
61
+ ]}
62
+ />
63
+ ```
64
+
65
+ `toneSelected` tones the **selected option only** — every unselected ring stays
66
+ neutral grey. Reach for it instead of `tone` when a resting tinted border would read
67
+ as a validation error. Same tone set; set it on the group, or on a single option to
68
+ override one.
69
+
70
+ To color the **label and hint**, there's no prop — add a `color` rule on the
71
+ option's `a-radio-label` / `a-radio-hint` with a `--text-N-{tone}` value.
72
+
73
+ ## Size
74
+
75
+ ```tsx
76
+ {/* Usually one size for the whole group: */}
77
+ <RadioGroup
78
+ size="small"
79
+ defaultValue="a"
80
+ options={[
81
+ { value: 'a', label: 'Option A' },
82
+ { value: 'b', label: 'Option B' },
83
+ ]}
84
+ />
85
+ ```
86
+
87
+ Three sizes scale the control circle **and** the label + hint type together:
88
+ `small` (14px ring / 13px label), `medium` (16px / 15px, the default), and
89
+ `large` (18px / 17px). Set it on the `RadioGroup`, or on a single option to
90
+ override one.
91
+
92
+ ## Orientation and layout
93
+
94
+ ```tsx
95
+ <RadioGroup
96
+ orientation="horizontal"
97
+ defaultValue="yes"
98
+ options={[
99
+ { value: 'yes', label: 'Yes' },
100
+ { value: 'no', label: 'No' },
101
+ { value: 'maybe', label: 'Maybe' },
102
+ ]}
103
+ />
104
+ ```
105
+
106
+ `vertical` (the default) stacks the options; `horizontal` lays them in a row.
107
+ Arrow keys move the selection either way.
108
+
109
+ For anything beyond that, the options live in a plain light-DOM `<a-radio-list>`,
110
+ so you can restyle their arrangement with ordinary CSS — no shadow parts:
111
+
112
+ ```css
113
+ /* e.g. a two-column grid */
114
+ a-radio-group a-radio-list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; }
115
+ ```
116
+
117
+ ## Disabled
118
+
119
+ ```tsx
120
+ {/* `disabled` on one option, or on the group to disable the whole set */}
121
+ <RadioGroup
122
+ defaultValue="checked"
123
+ options={[
124
+ { value: 'checked', label: 'Checked' },
125
+ { value: 'unchecked', label: 'Unchecked', disabled: true },
126
+ ]}
127
+ />
128
+ ```
129
+
130
+ Set `disabled` on a single option to disable just that one, or on the
131
+ `RadioGroup` to disable the whole set. Disabled options are skipped by keyboard
132
+ navigation and dropped from the tab order.
133
+
134
+ ## Validation
135
+
136
+ ```tsx
137
+ <RadioGroup
138
+ label="Pick a plan"
139
+ hint="Please choose a plan to continue."
140
+ status="critical"
141
+ options={[
142
+ { value: 'free', label: 'Free' },
143
+ { value: 'pro', label: 'Pro' },
144
+ ]}
145
+ />
146
+ ```
147
+
148
+ `status` recolors the group `hint` for feedback — the same tone set as
149
+ [`Input`](./input.md): `critical`, `warning`, `success`, `info`, `brand`
150
+ (omit for neutral). It styles the message only; drive it from your own validation.
151
+
152
+ ## Keyboard and accessibility
153
+
154
+ The group is one Tab stop: `Tab` moves into the selected option (or the first
155
+ enabled one), the arrow keys (`↑`/`↓`/`←`/`→`) move the selection between enabled
156
+ options (wrapping at the ends), and `Space` / `Enter` select the focused option.
157
+ Follows the [WAI-ARIA radio-group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
158
+
159
+ Under the hood the split is deliberate. The `<a-radio-group>` web component
160
+ **never mutates the DOM** — it coordinates entirely off-DOM: selection is set as a
161
+ property on each `<a-radio>` (which reflects it into `:state(selected)` +
162
+ `aria-checked` via its own `ElementInternals`), the form value goes through
163
+ `ElementInternals`, and focus is tracked with `aria-activedescendant`. The
164
+ **roving `tabindex`** — which option Tab lands on — is the one piece rendered
165
+ declaratively, by the `RadioGroup` wrapper, from the current value. That's why
166
+ selection is owned by the element but the wrapper holds the value: it needs it to
167
+ place the tab stop. (Hand-assembling raw `<a-radio-group>` / `<a-radio>` works
168
+ too; make the group the tab stop with `tabindex="0"` and it falls back to
169
+ `aria-activedescendant` for keyboard nav.)
170
+
171
+ ## Forms
172
+
173
+ `<a-radio-group>` is a form-associated custom element. Give it a `name` and the
174
+ selected option's `value` is submitted with the form like a native radio group;
175
+ form reset restores the `defaultValue` selection.
176
+
177
+ ```tsx
178
+ <form>
179
+ <RadioGroup name="contact" defaultValue="email" options={[
180
+ { value: 'email', label: 'Email' },
181
+ { value: 'sms', label: 'SMS' },
182
+ ]} />
183
+ </form>
184
+ // FormData → contact=email
185
+ ```
186
+
187
+ ## Controlled vs. uncontrolled
188
+
189
+ **Uncontrolled** — pass `defaultValue` and let the group own selection:
190
+
191
+ ```tsx
192
+ <RadioGroup name="plan" defaultValue="pro" options={[
193
+ { value: 'free', label: 'Free' },
194
+ { value: 'pro', label: 'Pro' },
195
+ { value: 'team', label: 'Team' },
196
+ ]} />
197
+ ```
198
+
199
+ **Controlled** — pass `value` + `onStateChange` and own it yourself. The group
200
+ fires *before* applying, so you accept by updating `value` and reject by doing
201
+ nothing (or `event.preventDefault()` to veto in uncontrolled mode):
202
+
203
+ ```tsx
204
+ const [plan, setPlan] = useState('pro')
205
+
206
+ <RadioGroup
207
+ name="plan"
208
+ value={plan}
209
+ onStateChange={(e, { next }) => setPlan(next)}
210
+ options={[
211
+ { value: 'free', label: 'Free' },
212
+ { value: 'pro', label: 'Pro' },
213
+ { value: 'team', label: 'Team' },
214
+ ]}
215
+ />
216
+ ```
217
+
218
+ `onStateChange`'s `detail` carries a `reason`: `'user'` for a pick (the only
219
+ **cancelable** one — `preventDefault()` vetoes it), and `'reset'` / `'restore'`
220
+ when a `<form>` reset or a bfcache/autofill restore moves the selection. Filter on
221
+ `reason` if you only want user picks; `next` is `null` when nothing is selected.
222
+
223
+ ## Events
224
+
225
+ Three callbacks, in firing order:
226
+
227
+ | Callback | When | Cancelable | Payload |
228
+ |---|---|---|---|
229
+ | `onStateChange(e, { next, prev, reason })` | **before** a pick applies | ✅ (user picks only) | next / prev value + `reason` |
230
+ | `onChange(e)` | **after** selection applies | — | native `change` event |
231
+ | `onValueChange(e, attrs)` | **after** selection applies | — | `{ value, name }` |
232
+
233
+ Reach for **`onStateChange`** to intercept/veto or to filter on `reason` (reset vs
234
+ restore vs user); for the everyday "react to the new value", use **`onValueChange`**.
235
+ `onChange` is the plain native hook. **`onFocus` / `onBlur`** report focus entering
236
+ and leaving the *group* (wired to `focusin` / `focusout`, since focus lands on an
237
+ individual option).
238
+
239
+ The group has `onStateChange` because its selection is a **discrete** state (one
240
+ of a fixed set) the element self-applies — so a transition can be vetoed before it
241
+ lands, and it carries `reason`. Value-based composites without a self-applying
242
+ element — `Input`, and the composed `Select` — skip it and expose only
243
+ `onValueChange`; the `onValueChange` shape is the same across all of them.
244
+
245
+ ```tsx
246
+ <RadioGroup onValueChange={(_e, { value }) => save(value)} options={…} />
247
+ ```
248
+
249
+ | Prop | Type | Default | Description |
250
+ |------|------|---------|-------------|
251
+ | `options` | RadioOption[] | — | The options. The wrapper renders one `<a-radio>` per entry and computes its
252
+ `selected` / roving `tabindex` / `role` declaratively. |
253
+ | `defaultValue?` | string | — | Initial selected value for uncontrolled use. |
254
+ | `disabled?` | boolean | — | Disable the whole group. |
255
+ | `hint?` | string | — | Plain-text description for the group, rendered directly under `label` (above
256
+ the options) — typically instructional copy. Per-option helper text goes on
257
+ the option's own `hint` instead. |
258
+ | `label?` | string | — | Plain-text label for the whole group, rendered above the options. |
259
+ | `name?` | string | — | Form field name — the group submits one `name=value` (it's the
260
+ form-associated element). |
261
+ | `onBlur?` | (event) => void | — | Fired when focus leaves the group entirely — wired to `focusout`. |
262
+ | `onChange?` | (event) => void | — | Fired *after* the selection changes — a native `change` event (the post-apply
263
+ counterpart to `onStateChange`). Not cancelable; for a controlled group it
264
+ fires once you've updated `value`. |
265
+ | `onFocus?` | (event) => void | — | Fired when focus enters the group (any option) — wired to `focusin`, since
266
+ focus lands on an individual option, not the group element itself. |
267
+ | `onStateChange?` | (event, detail) => void | — | Fired whenever selection changes — event-first. `detail` is
268
+ `{ next, prev, reason }`: `next`/`prev` are values (`null` = nothing selected);
269
+ `reason` is `'user'` \| `'reset'` \| `'restore'`. A `'user'` pick fires *before*
270
+ applying and is **cancelable** — `event.preventDefault()` vetoes it
271
+ (uncontrolled), or in controlled mode answer by updating `value` (reject by
272
+ doing nothing). `'reset'` (form reset) and `'restore'` (bfcache / autofill) are
273
+ not cancelable — filter on `reason` if you only track user picks. |
274
+ | `onValueChange?` | (event, attrs) => void | — | Like `onChange`, but with a `{ value, name }` snapshot as the second argument,
275
+ matching `Input`. |
276
+ | `orientation?` | 'vertical' \| 'horizontal' | 'vertical' | Layout + arrow-key axis. |
277
+ | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size applied to every option (an option's own `size` wins). |
278
+ | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | 'neutral' | Validation/feedback tone for the group `hint` — recolors it (same tone set
279
+ as `Input`'s `status`). Use `critical` for an error message, etc.; omit for
280
+ the neutral default. |
281
+ | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Mark tone applied to every option (an option's own `tone` wins), or any literal
282
+ CSS color for a one-off custom tone. Colors the selected-ring fill + dot *and*
283
+ the unselected ring border. Named tones track light/dark mode. Use `toneSelected`
284
+ instead to tone only the selected option and leave the rest neutral. The option
285
+ text stays neutral — recolor it in plain CSS via the `--text-N-{tone}` tokens. |
286
+ | `toneSelected?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Like `tone`, but colored onto the **selected option only** — every unselected
287
+ ring stays neutral grey. Applied to every option (an option's own `toneSelected`
288
+ wins). Prefer this over `tone` when a resting tinted border would read as a
289
+ validation state. |
290
+ | `value?` | string | — | Controlled selected value. The group follows this prop and reports a
291
+ requested change through `onStateChange`. Leave it undefined for
292
+ uncontrolled use. |
293
+
294
+ Use the web component directly when you are not using React or Preact and a native control does not fit.
295
+
296
+ The group owns selection and the single tab stop. Put options in `<a-radio-list>`.
297
+
298
+ ```html
299
+ <a-radio-group role="radiogroup" tabindex="0" name="plan" default-state="pro">
300
+ <a-radio-group-label>Plan</a-radio-group-label>
301
+ <a-radio-list>
302
+ <a-radio role="radio" value="starter"><a-radio-label>Starter</a-radio-label></a-radio>
303
+ <a-radio role="radio" value="pro"><a-radio-label>Pro</a-radio-label></a-radio>
304
+ </a-radio-list>
305
+ </a-radio-group>
306
+ ```
307
+
308
+ ### Native HTML radio inputs
309
+
310
+ For a standard HTML form, add `data-anta` to each native radio. Radios that share
311
+ a `name` retain the browser's selection, keyboard behavior, labels, and form
312
+ submission while using Anta's control surface.
313
+
314
+ `size`, `tone`, and `tone-selected` use the matching RadioGroup option
315
+ treatments.
316
+
317
+ ```html
318
+ <div style="display: grid; gap: 8px">
319
+ <label><input data-anta type="radio" name="plan" value="starter" size="small" tone="brand"> Starter</label>
320
+ <label><input data-anta type="radio" name="plan" value="pro" tone-selected="success" checked> Pro</label>
321
+ <label><input data-anta type="radio" name="plan" value="enterprise" size="large" tone="warning"> Enterprise</label>
322
+ </div>
323
+ ```
324
+
325
+ Reach for the props first: **`tone`** colors the mark in every state,
326
+ **`toneSelected`** only the selected option (any CSS color for a custom tone, set on
327
+ the group or a single option — derives the full curve in oklch), **`size`** the
328
+ dimensions + type. To tint the label + hint, add a `color` rule on the option's
329
+ `a-radio-label` / `a-radio-hint` with the `--text-N-{tone}` values — there's no
330
+ text-tone prop. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
331
+
332
+ ```tsx
333
+ <RadioGroup tone="#e0457b" defaultValue="a" options={[{ value: 'a', label: 'A' }]} />
334
+ ```
335
+
336
+ For anything else, `<a-radio>` is light-DOM — the **ring is `::before`**, the **dot
337
+ is `::after`**, the label/hint are the `a-radio-label` / `a-radio-hint` children.
338
+ Target them with ordinary CSS, **per state** (an un-layered rule beats `@layer anta`
339
+ without `!important`). The groups below push it — an outlined (unfilled) selected
340
+ ring, square black/white marks, and a bigger ring with a heart-shaped mark (the dot,
341
+ masked — same heart as the checkbox):
342
+
343
+ ```css
344
+ /* Outlined — an unfilled selected ring: keep the ring background and paint the
345
+ border + dot in the tone color (light-dark() tunes both themes at once). */
346
+ a-radio-group.outlined a-radio:state(selected)::before {
347
+ background: var(--radio-bg);
348
+ border-color: light-dark(#5f4bc3, #7460d7);
349
+ }
350
+ a-radio-group.outlined a-radio:state(selected)::after { background: light-dark(#5f4bc3, #7460d7); }
351
+
352
+ /* Square — square ring + square dot, black/white per theme via light-dark() */
353
+ a-radio-group.square a-radio::before,
354
+ a-radio-group.square a-radio::after { border-radius: 0; }
355
+ a-radio-group.square a-radio:state(selected)::before { background: light-dark(#000, #fff); border-color: light-dark(#000, #fff); }
356
+ a-radio-group.square a-radio:state(selected)::after { background: light-dark(#fff, #000); } /* contrasting dot */
357
+
358
+ /* Heart — a bigger ring with a heart mark (the dot ::after, masked) on a pink fill.
359
+ Same heart SVG as the checkbox; mask-size: contain centers it in the bigger ring. */
360
+ a-radio-group.heart a-radio::before { inline-size: 22px; block-size: 22px; } /* bigger ring */
361
+ a-radio-group.heart a-radio::after {
362
+ inline-size: 13px; block-size: 13px; border-radius: 0;
363
+ mask-image: url("data:image/svg+xml,%3Csvg …a heart path… /%3E");
364
+ mask-size: contain; mask-repeat: no-repeat; mask-position: center;
365
+ }
366
+ a-radio-group.heart a-radio:state(selected)::before { background: light-dark(#ec4899, #f472b6); border-color: transparent; }
367
+ a-radio-group.heart a-radio:state(selected)::after { background: #fff; } /* white heart */
368
+ ```