@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,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' | Colour variant, or any literal CSS colour 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 — recolour 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,369 @@
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 & 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` colours 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 — colour them in plain CSS with the `--text-N-{tone}` tokens. Set `tone` on
49
+ the `RadioGroup` to colour every option, or on a single option to override just that
50
+ one. Any literal CSS colour 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 colour 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 & 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` recolours 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 & 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 the uncontrolled case (the wrapper then owns the
254
+ selection in local state). |
255
+ | `disabled?` | boolean | — | Disable the whole group. |
256
+ | `hint?` | string | — | Plain-text description for the group, rendered directly under `label` (above
257
+ the options) — typically instructional copy. Per-option helper text goes on
258
+ the option's own `hint` instead. |
259
+ | `label?` | string | — | Plain-text label for the whole group, rendered above the options. |
260
+ | `name?` | string | — | Form field name — the group submits one `name=value` (it's the
261
+ form-associated element). |
262
+ | `onBlur?` | (event) => void | — | Fired when focus leaves the group entirely — wired to `focusout`. |
263
+ | `onChange?` | (event) => void | — | Fired *after* the selection changes — a native `change` event (the post-apply
264
+ counterpart to `onStateChange`). Not cancelable; for a controlled group it
265
+ fires once you've updated `value`. |
266
+ | `onFocus?` | (event) => void | — | Fired when focus enters the group (any option) — wired to `focusin`, since
267
+ focus lands on an individual option, not the group element itself. |
268
+ | `onStateChange?` | (event, detail) => void | — | Fired whenever selection changes — event-first. `detail` is
269
+ `{ next, prev, reason }`: `next`/`prev` are values (`null` = nothing selected);
270
+ `reason` is `'user'` \| `'reset'` \| `'restore'`. A `'user'` pick fires *before*
271
+ applying and is **cancelable** — `event.preventDefault()` vetoes it
272
+ (uncontrolled), or in controlled mode answer by updating `value` (reject by
273
+ doing nothing). `'reset'` (form reset) and `'restore'` (bfcache / autofill) are
274
+ not cancelable — filter on `reason` if you only track user picks. |
275
+ | `onValueChange?` | (event, attrs) => void | — | Like `onChange`, but with a `{ value, name }` snapshot as the 2nd argument —
276
+ the ergonomic "just give me the new value" callback (mirrors `Input`). |
277
+ | `orientation?` | 'vertical' \| 'horizontal' | 'vertical' | Layout + arrow-key axis. |
278
+ | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size applied to every option (an option's own `size` wins). |
279
+ | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | 'neutral' | Validation/feedback tone for the group `hint` — recolours it (same tone set
280
+ as `Input`'s `status`). Use `critical` for an error message, etc.; omit for
281
+ the neutral default. |
282
+ | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Mark tone applied to every option (an option's own `tone` wins), or any literal
283
+ CSS color for a one-off custom tone. Colours the selected-ring fill + dot *and*
284
+ the unselected ring border. Named tones track light/dark mode. Use `toneSelected`
285
+ instead to tone only the selected option and leave the rest neutral. The option
286
+ text stays neutral — recolour it in plain CSS via the `--text-N-{tone}` tokens. |
287
+ | `toneSelected?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Like `tone`, but coloured onto the **selected option only** — every unselected
288
+ ring stays neutral grey. Applied to every option (an option's own `toneSelected`
289
+ wins). Prefer this over `tone` when a resting tinted border would read as a
290
+ validation state. |
291
+ | `value?` | string | — | Controlled selected value. When provided, the consumer owns selection: the
292
+ group follows this prop and a pick only *requests* a change via
293
+ `onStateChange` (reject by not updating). Leave undefined for uncontrolled. |
294
+
295
+ 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.
296
+
297
+ The group owns selection and the single tab stop. Put options in `<a-radio-list>`.
298
+
299
+ ```html
300
+ <a-radio-group role="radiogroup" tabindex="0" name="plan" default-state="pro">
301
+ <a-radio-group-label>Plan</a-radio-group-label>
302
+ <a-radio-list>
303
+ <a-radio role="radio" value="starter"><a-radio-label>Starter</a-radio-label></a-radio>
304
+ <a-radio role="radio" value="pro"><a-radio-label>Pro</a-radio-label></a-radio>
305
+ </a-radio-list>
306
+ </a-radio-group>
307
+ ```
308
+
309
+ ### Native HTML radio inputs
310
+
311
+ For a standard HTML form, add `data-anta` to each native radio. Radios that share
312
+ a `name` retain the browser's selection, keyboard behavior, labels, and form
313
+ submission while using Anta's control surface.
314
+
315
+ `size`, `tone`, and `tone-selected` use the matching RadioGroup option
316
+ treatments.
317
+
318
+ ```html
319
+ <div style="display: grid; gap: 8px">
320
+ <label><input data-anta type="radio" name="plan" value="starter" size="small" tone="brand"> Starter</label>
321
+ <label><input data-anta type="radio" name="plan" value="pro" tone-selected="success" checked> Pro</label>
322
+ <label><input data-anta type="radio" name="plan" value="enterprise" size="large" tone="warning"> Enterprise</label>
323
+ </div>
324
+ ```
325
+
326
+ Reach for the props first: **`tone`** colours the mark in every state,
327
+ **`toneSelected`** only the selected option (any CSS colour for a custom tone, set on
328
+ the group or a single option — derives the full curve in oklch), **`size`** the
329
+ dimensions + type. To tint the label + hint, add a `color` rule on the option's
330
+ `a-radio-label` / `a-radio-hint` with the `--text-N-{tone}` values — there's no
331
+ text-tone prop. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
332
+
333
+ ```tsx
334
+ <RadioGroup tone="#e0457b" defaultValue="a" options={[{ value: 'a', label: 'A' }]} />
335
+ ```
336
+
337
+ For anything else, `<a-radio>` is light-DOM — the **ring is `::before`**, the **dot
338
+ is `::after`**, the label/hint are the `a-radio-label` / `a-radio-hint` children.
339
+ Target them with ordinary CSS, **per state** (an un-layered rule beats `@layer anta`
340
+ without `!important`). The groups below push it — an outlined (unfilled) selected
341
+ ring, square black/white marks, and a bigger ring with a heart-shaped mark (the dot,
342
+ masked — same heart as the checkbox):
343
+
344
+ ```css
345
+ /* Outlined — an unfilled selected ring: keep the ring background and paint the
346
+ border + dot in the tone colour (light-dark() tunes both themes at once). */
347
+ a-radio-group.outlined a-radio:state(selected)::before {
348
+ background: var(--radio-bg);
349
+ border-color: light-dark(#5f4bc3, #7460d7);
350
+ }
351
+ a-radio-group.outlined a-radio:state(selected)::after { background: light-dark(#5f4bc3, #7460d7); }
352
+
353
+ /* Square — square ring + square dot, black/white per theme via light-dark() */
354
+ a-radio-group.square a-radio::before,
355
+ a-radio-group.square a-radio::after { border-radius: 0; }
356
+ a-radio-group.square a-radio:state(selected)::before { background: light-dark(#000, #fff); border-color: light-dark(#000, #fff); }
357
+ a-radio-group.square a-radio:state(selected)::after { background: light-dark(#fff, #000); } /* contrasting dot */
358
+
359
+ /* Heart — a bigger ring with a heart mark (the dot ::after, masked) on a pink fill.
360
+ Same heart SVG as the checkbox; mask-size: contain centres it in the bigger ring. */
361
+ a-radio-group.heart a-radio::before { inline-size: 22px; block-size: 22px; } /* bigger ring */
362
+ a-radio-group.heart a-radio::after {
363
+ inline-size: 13px; block-size: 13px; border-radius: 0;
364
+ mask-image: url("data:image/svg+xml,%3Csvg …a heart path… /%3E");
365
+ mask-size: contain; mask-repeat: no-repeat; mask-position: center;
366
+ }
367
+ a-radio-group.heart a-radio:state(selected)::before { background: light-dark(#ec4899, #f472b6); border-color: transparent; }
368
+ a-radio-group.heart a-radio:state(selected)::after { background: #fff; } /* white heart */
369
+ ```