@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.
- package/README.md +38 -242
- package/dist/anta_helpers.d.ts +2 -2
- package/dist/components/Banner.d.ts +3 -3
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Calendar.d.ts +11 -12
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +1 -1
- package/dist/components/Checkbox.d.ts +8 -9
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Expander.d.ts +5 -6
- package/dist/components/Input.d.ts +6 -3
- package/dist/components/Input.js +3 -1
- package/dist/components/InputAutocomplete.d.ts +8 -8
- package/dist/components/InputDate.css +1 -0
- package/dist/components/InputDate.d.ts +1 -0
- package/dist/components/InputDate.js +5 -5
- package/dist/components/InputTime.d.ts +3 -3
- package/dist/components/InputTime.js +1 -1
- package/dist/components/Menu.d.ts +7 -3
- package/dist/components/Menu.js +3 -2
- package/dist/components/MenuItem.d.ts +1 -1
- package/dist/components/MenuItem.js +1 -1
- package/dist/components/Progress.d.ts +1 -1
- package/dist/components/RadioGroup.d.ts +12 -13
- package/dist/components/Select.d.ts +32 -38
- package/dist/components/Select.js +9 -13
- package/dist/components/SelectFaceted.d.ts +28 -33
- package/dist/components/SelectFaceted.js +29 -23
- package/dist/components/Slider.d.ts +5 -5
- package/dist/components/Switch.d.ts +4 -4
- package/dist/components/Tabs.d.ts +7 -7
- package/dist/components/Title.d.ts +1 -1
- package/dist/components/Toaster.d.ts +1 -1
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/select-parts.css +1 -0
- package/dist/elements/a-card.css +1 -1
- package/dist/elements/a-dialog.css +1 -1
- package/dist/elements/a-expander.css +1 -1
- package/dist/elements/a-expander.d.ts +1 -0
- package/dist/elements/a-expander.js +12 -2
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-menu.js +12 -0
- package/dist/elements/a-progress.css +1 -1
- package/dist/elements/a-slider.css +1 -1
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tabs.d.ts +3 -0
- package/dist/elements/a-tabs.js +40 -1
- package/dist/elements/a-tooltip.css +1 -1
- package/dist/elements/a-tooltip.d.ts +1 -1
- package/dist/elements/a-tooltip.js +1 -1
- package/dist/elements/index.d.ts +1 -1
- package/dist/general_types.d.ts +24 -22
- package/dist/index.d.ts +3 -3
- package/dist/jsx-runtime.d.ts +9 -0
- package/docs/accessibility.md +34 -0
- package/docs/changelog.md +561 -0
- package/docs/colors.md +42 -0
- package/docs/comparison.md +28 -0
- package/docs/components/banner.md +267 -0
- package/docs/components/button.md +603 -0
- package/docs/components/card.md +321 -0
- package/docs/components/checkbox.md +296 -0
- package/docs/components/dialog.md +314 -0
- package/docs/components/expander.md +526 -0
- package/docs/components/icon.md +155 -0
- package/docs/components/input-autocomplete.md +100 -0
- package/docs/components/input-date.md +401 -0
- package/docs/components/input-time.md +174 -0
- package/docs/components/input.md +610 -0
- package/docs/components/loader.md +139 -0
- package/docs/components/menu.md +511 -0
- package/docs/components/progress.md +182 -0
- package/docs/components/radio.md +368 -0
- package/docs/components/select-faceted.md +451 -0
- package/docs/components/select.md +926 -0
- package/docs/components/slider.md +147 -0
- package/docs/components/switch.md +198 -0
- package/docs/components/tabs.md +471 -0
- package/docs/components/tag.md +185 -0
- package/docs/components/text.md +148 -0
- package/docs/components/title.md +146 -0
- package/docs/components/toaster.md +313 -0
- package/docs/components/tooltip.md +177 -0
- package/docs/credits.md +18 -0
- package/docs/index.md +52 -0
- package/docs/install-config.md +240 -0
- package/docs/normalization.md +102 -0
- package/docs/overview.md +10 -0
- package/docs/packages/stickers.md +187 -0
- package/docs/packages/table.md +153 -0
- package/docs/theming.md +13 -0
- 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
|
+
```
|