@discourser/design-system 0.3.0 → 0.4.0
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/guidelines/Guidelines.md +195 -0
- package/guidelines/components/accordion.md +639 -0
- package/guidelines/components/avatar.md +945 -0
- package/guidelines/components/badge.md +667 -0
- package/guidelines/components/button.md +314 -0
- package/guidelines/components/card.md +353 -0
- package/guidelines/components/checkbox.md +583 -0
- package/guidelines/components/dialog.md +465 -0
- package/guidelines/components/drawer.md +961 -0
- package/guidelines/components/heading.md +505 -0
- package/guidelines/components/icon-button.md +417 -0
- package/guidelines/components/input.md +499 -0
- package/guidelines/components/popover.md +1200 -0
- package/guidelines/components/progress.md +773 -0
- package/guidelines/components/radio-group.md +757 -0
- package/guidelines/components/select.md +1155 -0
- package/guidelines/components/skeleton.md +726 -0
- package/guidelines/components/switch.md +457 -0
- package/guidelines/components/tabs.md +834 -0
- package/guidelines/components/textarea.md +425 -0
- package/guidelines/components/toast.md +707 -0
- package/guidelines/components/tooltip.md +832 -0
- package/guidelines/design-tokens/colors.md +187 -0
- package/guidelines/design-tokens/elevation.md +274 -0
- package/guidelines/design-tokens/spacing.md +289 -0
- package/guidelines/design-tokens/typography.md +226 -0
- package/guidelines/overview-components.md +204 -0
- package/package.json +3 -2
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
# RadioGroup
|
|
2
|
+
|
|
3
|
+
**Purpose:** Provides mutually exclusive selection between multiple options, allowing users to choose exactly one item from a set of choices.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { RadioGroup } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Component Structure
|
|
12
|
+
|
|
13
|
+
RadioGroup is a compound component built on Ark UI, providing a complete solution for radio button groups with built-in accessibility and state management.
|
|
14
|
+
|
|
15
|
+
### Anatomy
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
<RadioGroup.Root>
|
|
19
|
+
<RadioGroup.Label />
|
|
20
|
+
<RadioGroup.Item>
|
|
21
|
+
<RadioGroup.ItemControl>
|
|
22
|
+
<RadioGroup.Indicator />
|
|
23
|
+
</RadioGroup.ItemControl>
|
|
24
|
+
<RadioGroup.ItemText />
|
|
25
|
+
<RadioGroup.ItemHiddenInput />
|
|
26
|
+
</RadioGroup.Item>
|
|
27
|
+
</RadioGroup.Root>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Component Parts:**
|
|
31
|
+
|
|
32
|
+
- **Root**: Container that manages the radio group state and keyboard navigation
|
|
33
|
+
- **Label**: Optional label for the entire group
|
|
34
|
+
- **Item**: Individual radio option container
|
|
35
|
+
- **ItemControl**: Visual radio button (circle with inner dot when selected)
|
|
36
|
+
- **Indicator**: Visual indicator shown when radio is selected
|
|
37
|
+
- **ItemText**: Label text for the individual radio option
|
|
38
|
+
- **ItemHiddenInput**: Hidden native input for form integration and accessibility
|
|
39
|
+
|
|
40
|
+
## Variants
|
|
41
|
+
|
|
42
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
43
|
+
| ------- | ------------------------------------------------- | ---------------------- | ---------------------------------------------- |
|
|
44
|
+
| `solid` | Filled circle with color background when selected | Standard radio buttons | All use cases, default choice for radio groups |
|
|
45
|
+
|
|
46
|
+
**Note:** Currently only the `solid` variant is implemented, providing a clean, Material Design-inspired appearance.
|
|
47
|
+
|
|
48
|
+
### Visual Characteristics
|
|
49
|
+
|
|
50
|
+
- **solid**: Gray border circle in default state, colored background with white inner dot when selected
|
|
51
|
+
|
|
52
|
+
## Sizes
|
|
53
|
+
|
|
54
|
+
| Size | Control Size | Gap | Font Size | Usage |
|
|
55
|
+
| ---- | ------------ | -------- | --------- | --------------------------------------------------------- |
|
|
56
|
+
| `sm` | 18px (4.5) | 8px (2) | sm | Compact forms, dense layouts, space-constrained UI |
|
|
57
|
+
| `md` | 20px (5) | 12px (3) | md | Default, most use cases, standard forms |
|
|
58
|
+
| `lg` | 22px (5.5) | 12px (3) | lg | Touch-friendly interfaces, emphasis, mobile-first designs |
|
|
59
|
+
|
|
60
|
+
**Recommendation:** Use `md` for most cases. Use `lg` for mobile-first designs or when prioritizing touch accessibility.
|
|
61
|
+
|
|
62
|
+
## Orientation
|
|
63
|
+
|
|
64
|
+
RadioGroup supports both horizontal and vertical layouts:
|
|
65
|
+
|
|
66
|
+
| Orientation | Layout Direction | When to Use |
|
|
67
|
+
| ------------ | --------------------- | ---------------------------------------------- |
|
|
68
|
+
| `vertical` | Stacked vertically | Default, most forms, multiple options (3+) |
|
|
69
|
+
| `horizontal` | Arranged horizontally | Simple choices (2-3 options), toolbar settings |
|
|
70
|
+
|
|
71
|
+
## Props
|
|
72
|
+
|
|
73
|
+
### Root Props
|
|
74
|
+
|
|
75
|
+
| Prop | Type | Default | Description |
|
|
76
|
+
| --------------- | -------------------------------------- | ------------ | --------------------------------------- |
|
|
77
|
+
| `variant` | `'solid'` | `'solid'` | Visual style variant |
|
|
78
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Control and text size |
|
|
79
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction |
|
|
80
|
+
| `value` | `string` | - | Currently selected value (controlled) |
|
|
81
|
+
| `defaultValue` | `string` | - | Initially selected value (uncontrolled) |
|
|
82
|
+
| `disabled` | `boolean` | `false` | Disable entire radio group |
|
|
83
|
+
| `onValueChange` | `(details: { value: string }) => void` | - | Callback when selection changes |
|
|
84
|
+
| `name` | `string` | - | Form field name |
|
|
85
|
+
|
|
86
|
+
### Item Props
|
|
87
|
+
|
|
88
|
+
| Prop | Type | Default | Description |
|
|
89
|
+
| ---------- | --------- | -------- | --------------------------------- |
|
|
90
|
+
| `value` | `string` | Required | Unique identifier for this option |
|
|
91
|
+
| `disabled` | `boolean` | `false` | Disable this specific option |
|
|
92
|
+
| `invalid` | `boolean` | `false` | Mark this option as invalid |
|
|
93
|
+
|
|
94
|
+
**Note:** RadioGroup.Root extends Ark UI's RadioGroupRootProps, supporting all native radio group attributes.
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
### Basic Usage
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Uncontrolled (internal state management)
|
|
102
|
+
<RadioGroup.Root defaultValue="option1">
|
|
103
|
+
<RadioGroup.Label>Choose an option</RadioGroup.Label>
|
|
104
|
+
<RadioGroup.Item value="option1">
|
|
105
|
+
<RadioGroup.ItemControl>
|
|
106
|
+
<RadioGroup.Indicator />
|
|
107
|
+
</RadioGroup.ItemControl>
|
|
108
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
109
|
+
<RadioGroup.ItemHiddenInput />
|
|
110
|
+
</RadioGroup.Item>
|
|
111
|
+
<RadioGroup.Item value="option2">
|
|
112
|
+
<RadioGroup.ItemControl>
|
|
113
|
+
<RadioGroup.Indicator />
|
|
114
|
+
</RadioGroup.ItemControl>
|
|
115
|
+
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
|
|
116
|
+
<RadioGroup.ItemHiddenInput />
|
|
117
|
+
</RadioGroup.Item>
|
|
118
|
+
<RadioGroup.Item value="option3">
|
|
119
|
+
<RadioGroup.ItemControl>
|
|
120
|
+
<RadioGroup.Indicator />
|
|
121
|
+
</RadioGroup.ItemControl>
|
|
122
|
+
<RadioGroup.ItemText>Option 3</RadioGroup.ItemText>
|
|
123
|
+
<RadioGroup.ItemHiddenInput />
|
|
124
|
+
</RadioGroup.Item>
|
|
125
|
+
</RadioGroup.Root>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Controlled Usage
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const [selectedValue, setSelectedValue] = useState('medium');
|
|
132
|
+
|
|
133
|
+
<RadioGroup.Root
|
|
134
|
+
value={selectedValue}
|
|
135
|
+
onValueChange={(details) => setSelectedValue(details.value)}
|
|
136
|
+
>
|
|
137
|
+
<RadioGroup.Label>Select size</RadioGroup.Label>
|
|
138
|
+
<RadioGroup.Item value="small">
|
|
139
|
+
<RadioGroup.ItemControl>
|
|
140
|
+
<RadioGroup.Indicator />
|
|
141
|
+
</RadioGroup.ItemControl>
|
|
142
|
+
<RadioGroup.ItemText>Small</RadioGroup.ItemText>
|
|
143
|
+
<RadioGroup.ItemHiddenInput />
|
|
144
|
+
</RadioGroup.Item>
|
|
145
|
+
<RadioGroup.Item value="medium">
|
|
146
|
+
<RadioGroup.ItemControl>
|
|
147
|
+
<RadioGroup.Indicator />
|
|
148
|
+
</RadioGroup.ItemControl>
|
|
149
|
+
<RadioGroup.ItemText>Medium</RadioGroup.ItemText>
|
|
150
|
+
<RadioGroup.ItemHiddenInput />
|
|
151
|
+
</RadioGroup.Item>
|
|
152
|
+
<RadioGroup.Item value="large">
|
|
153
|
+
<RadioGroup.ItemControl>
|
|
154
|
+
<RadioGroup.Indicator />
|
|
155
|
+
</RadioGroup.ItemControl>
|
|
156
|
+
<RadioGroup.ItemText>Large</RadioGroup.ItemText>
|
|
157
|
+
<RadioGroup.ItemHiddenInput />
|
|
158
|
+
</RadioGroup.Item>
|
|
159
|
+
</RadioGroup.Root>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Different Sizes
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Small size (compact)
|
|
166
|
+
<RadioGroup.Root size="sm" defaultValue="option1">
|
|
167
|
+
<RadioGroup.Label>Small Radio Group</RadioGroup.Label>
|
|
168
|
+
<RadioGroup.Item value="option1">
|
|
169
|
+
<RadioGroup.ItemControl>
|
|
170
|
+
<RadioGroup.Indicator />
|
|
171
|
+
</RadioGroup.ItemControl>
|
|
172
|
+
<RadioGroup.ItemText>Compact option</RadioGroup.ItemText>
|
|
173
|
+
<RadioGroup.ItemHiddenInput />
|
|
174
|
+
</RadioGroup.Item>
|
|
175
|
+
</RadioGroup.Root>
|
|
176
|
+
|
|
177
|
+
// Large size (touch-friendly)
|
|
178
|
+
<RadioGroup.Root size="lg" defaultValue="option1">
|
|
179
|
+
<RadioGroup.Label>Large Radio Group</RadioGroup.Label>
|
|
180
|
+
<RadioGroup.Item value="option1">
|
|
181
|
+
<RadioGroup.ItemControl>
|
|
182
|
+
<RadioGroup.Indicator />
|
|
183
|
+
</RadioGroup.ItemControl>
|
|
184
|
+
<RadioGroup.ItemText>Touch-friendly option</RadioGroup.ItemText>
|
|
185
|
+
<RadioGroup.ItemHiddenInput />
|
|
186
|
+
</RadioGroup.Item>
|
|
187
|
+
</RadioGroup.Root>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Horizontal Layout
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
<RadioGroup.Root orientation="horizontal" defaultValue="yes">
|
|
194
|
+
<RadioGroup.Label>Enable notifications?</RadioGroup.Label>
|
|
195
|
+
<RadioGroup.Item value="yes">
|
|
196
|
+
<RadioGroup.ItemControl>
|
|
197
|
+
<RadioGroup.Indicator />
|
|
198
|
+
</RadioGroup.ItemControl>
|
|
199
|
+
<RadioGroup.ItemText>Yes</RadioGroup.ItemText>
|
|
200
|
+
<RadioGroup.ItemHiddenInput />
|
|
201
|
+
</RadioGroup.Item>
|
|
202
|
+
<RadioGroup.Item value="no">
|
|
203
|
+
<RadioGroup.ItemControl>
|
|
204
|
+
<RadioGroup.Indicator />
|
|
205
|
+
</RadioGroup.ItemControl>
|
|
206
|
+
<RadioGroup.ItemText>No</RadioGroup.ItemText>
|
|
207
|
+
<RadioGroup.ItemHiddenInput />
|
|
208
|
+
</RadioGroup.Item>
|
|
209
|
+
</RadioGroup.Root>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Disabled States
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Entire group disabled
|
|
216
|
+
<RadioGroup.Root disabled defaultValue="option1">
|
|
217
|
+
<RadioGroup.Label>Disabled Group</RadioGroup.Label>
|
|
218
|
+
<RadioGroup.Item value="option1">
|
|
219
|
+
<RadioGroup.ItemControl>
|
|
220
|
+
<RadioGroup.Indicator />
|
|
221
|
+
</RadioGroup.ItemControl>
|
|
222
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
223
|
+
<RadioGroup.ItemHiddenInput />
|
|
224
|
+
</RadioGroup.Item>
|
|
225
|
+
<RadioGroup.Item value="option2">
|
|
226
|
+
<RadioGroup.ItemControl>
|
|
227
|
+
<RadioGroup.Indicator />
|
|
228
|
+
</RadioGroup.ItemControl>
|
|
229
|
+
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
|
|
230
|
+
<RadioGroup.ItemHiddenInput />
|
|
231
|
+
</RadioGroup.Item>
|
|
232
|
+
</RadioGroup.Root>
|
|
233
|
+
|
|
234
|
+
// Individual option disabled
|
|
235
|
+
<RadioGroup.Root defaultValue="option1">
|
|
236
|
+
<RadioGroup.Label>Partially Disabled</RadioGroup.Label>
|
|
237
|
+
<RadioGroup.Item value="option1">
|
|
238
|
+
<RadioGroup.ItemControl>
|
|
239
|
+
<RadioGroup.Indicator />
|
|
240
|
+
</RadioGroup.ItemControl>
|
|
241
|
+
<RadioGroup.ItemText>Available option</RadioGroup.ItemText>
|
|
242
|
+
<RadioGroup.ItemHiddenInput />
|
|
243
|
+
</RadioGroup.Item>
|
|
244
|
+
<RadioGroup.Item value="option2" disabled>
|
|
245
|
+
<RadioGroup.ItemControl>
|
|
246
|
+
<RadioGroup.Indicator />
|
|
247
|
+
</RadioGroup.ItemControl>
|
|
248
|
+
<RadioGroup.ItemText>Disabled option</RadioGroup.ItemText>
|
|
249
|
+
<RadioGroup.ItemHiddenInput />
|
|
250
|
+
</RadioGroup.Item>
|
|
251
|
+
</RadioGroup.Root>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Form Integration
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
<form onSubmit={handleSubmit}>
|
|
258
|
+
<RadioGroup.Root name="deliveryMethod" defaultValue="standard">
|
|
259
|
+
<RadioGroup.Label>Delivery Method</RadioGroup.Label>
|
|
260
|
+
<RadioGroup.Item value="standard">
|
|
261
|
+
<RadioGroup.ItemControl>
|
|
262
|
+
<RadioGroup.Indicator />
|
|
263
|
+
</RadioGroup.ItemControl>
|
|
264
|
+
<RadioGroup.ItemText>Standard (5-7 days)</RadioGroup.ItemText>
|
|
265
|
+
<RadioGroup.ItemHiddenInput />
|
|
266
|
+
</RadioGroup.Item>
|
|
267
|
+
<RadioGroup.Item value="express">
|
|
268
|
+
<RadioGroup.ItemControl>
|
|
269
|
+
<RadioGroup.Indicator />
|
|
270
|
+
</RadioGroup.ItemControl>
|
|
271
|
+
<RadioGroup.ItemText>Express (2-3 days)</RadioGroup.ItemText>
|
|
272
|
+
<RadioGroup.ItemHiddenInput />
|
|
273
|
+
</RadioGroup.Item>
|
|
274
|
+
<RadioGroup.Item value="overnight">
|
|
275
|
+
<RadioGroup.ItemControl>
|
|
276
|
+
<RadioGroup.Indicator />
|
|
277
|
+
</RadioGroup.ItemControl>
|
|
278
|
+
<RadioGroup.ItemText>Overnight</RadioGroup.ItemText>
|
|
279
|
+
<RadioGroup.ItemHiddenInput />
|
|
280
|
+
</RadioGroup.Item>
|
|
281
|
+
</RadioGroup.Root>
|
|
282
|
+
<Button type="submit">Continue</Button>
|
|
283
|
+
</form>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Common Patterns
|
|
287
|
+
|
|
288
|
+
### Dynamic Options from Data
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const deliveryOptions = [
|
|
292
|
+
{ value: 'standard', label: 'Standard Shipping', description: '5-7 business days' },
|
|
293
|
+
{ value: 'express', label: 'Express Shipping', description: '2-3 business days' },
|
|
294
|
+
{ value: 'overnight', label: 'Overnight Shipping', description: 'Next business day' },
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
<RadioGroup.Root defaultValue="standard">
|
|
298
|
+
<RadioGroup.Label>Choose delivery method</RadioGroup.Label>
|
|
299
|
+
{deliveryOptions.map((option) => (
|
|
300
|
+
<RadioGroup.Item key={option.value} value={option.value}>
|
|
301
|
+
<RadioGroup.ItemControl>
|
|
302
|
+
<RadioGroup.Indicator />
|
|
303
|
+
</RadioGroup.ItemControl>
|
|
304
|
+
<RadioGroup.ItemText>
|
|
305
|
+
<div>
|
|
306
|
+
<div>{option.label}</div>
|
|
307
|
+
<div className={css({ color: 'fg.subtle', fontSize: 'sm' })}>
|
|
308
|
+
{option.description}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</RadioGroup.ItemText>
|
|
312
|
+
<RadioGroup.ItemHiddenInput />
|
|
313
|
+
</RadioGroup.Item>
|
|
314
|
+
))}
|
|
315
|
+
</RadioGroup.Root>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### With Validation
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const [value, setValue] = useState('');
|
|
322
|
+
const [error, setError] = useState('');
|
|
323
|
+
|
|
324
|
+
const handleSubmit = () => {
|
|
325
|
+
if (!value) {
|
|
326
|
+
setError('Please select an option');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
setError('');
|
|
330
|
+
// Process form
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
<div>
|
|
334
|
+
<RadioGroup.Root
|
|
335
|
+
value={value}
|
|
336
|
+
onValueChange={(details) => {
|
|
337
|
+
setValue(details.value);
|
|
338
|
+
setError('');
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
<RadioGroup.Label>Select your preference</RadioGroup.Label>
|
|
342
|
+
<RadioGroup.Item value="option1">
|
|
343
|
+
<RadioGroup.ItemControl>
|
|
344
|
+
<RadioGroup.Indicator />
|
|
345
|
+
</RadioGroup.ItemControl>
|
|
346
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
347
|
+
<RadioGroup.ItemHiddenInput />
|
|
348
|
+
</RadioGroup.Item>
|
|
349
|
+
<RadioGroup.Item value="option2">
|
|
350
|
+
<RadioGroup.ItemControl>
|
|
351
|
+
<RadioGroup.Indicator />
|
|
352
|
+
</RadioGroup.ItemControl>
|
|
353
|
+
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
|
|
354
|
+
<RadioGroup.ItemHiddenInput />
|
|
355
|
+
</RadioGroup.Item>
|
|
356
|
+
</RadioGroup.Root>
|
|
357
|
+
{error && (
|
|
358
|
+
<div className={css({ color: 'error.fg', fontSize: 'sm', mt: '1' })}>
|
|
359
|
+
{error}
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Settings Panel
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: '6' })}>
|
|
369
|
+
<RadioGroup.Root defaultValue="light" name="theme">
|
|
370
|
+
<RadioGroup.Label>Theme Preference</RadioGroup.Label>
|
|
371
|
+
<RadioGroup.Item value="light">
|
|
372
|
+
<RadioGroup.ItemControl>
|
|
373
|
+
<RadioGroup.Indicator />
|
|
374
|
+
</RadioGroup.ItemControl>
|
|
375
|
+
<RadioGroup.ItemText>Light</RadioGroup.ItemText>
|
|
376
|
+
<RadioGroup.ItemHiddenInput />
|
|
377
|
+
</RadioGroup.Item>
|
|
378
|
+
<RadioGroup.Item value="dark">
|
|
379
|
+
<RadioGroup.ItemControl>
|
|
380
|
+
<RadioGroup.Indicator />
|
|
381
|
+
</RadioGroup.ItemControl>
|
|
382
|
+
<RadioGroup.ItemText>Dark</RadioGroup.ItemText>
|
|
383
|
+
<RadioGroup.ItemHiddenInput />
|
|
384
|
+
</RadioGroup.Item>
|
|
385
|
+
<RadioGroup.Item value="system">
|
|
386
|
+
<RadioGroup.ItemControl>
|
|
387
|
+
<RadioGroup.Indicator />
|
|
388
|
+
</RadioGroup.ItemControl>
|
|
389
|
+
<RadioGroup.ItemText>System</RadioGroup.ItemText>
|
|
390
|
+
<RadioGroup.ItemHiddenInput />
|
|
391
|
+
</RadioGroup.Item>
|
|
392
|
+
</RadioGroup.Root>
|
|
393
|
+
|
|
394
|
+
<RadioGroup.Root defaultValue="en" name="language">
|
|
395
|
+
<RadioGroup.Label>Language</RadioGroup.Label>
|
|
396
|
+
<RadioGroup.Item value="en">
|
|
397
|
+
<RadioGroup.ItemControl>
|
|
398
|
+
<RadioGroup.Indicator />
|
|
399
|
+
</RadioGroup.ItemControl>
|
|
400
|
+
<RadioGroup.ItemText>English</RadioGroup.ItemText>
|
|
401
|
+
<RadioGroup.ItemHiddenInput />
|
|
402
|
+
</RadioGroup.Item>
|
|
403
|
+
<RadioGroup.Item value="es">
|
|
404
|
+
<RadioGroup.ItemControl>
|
|
405
|
+
<RadioGroup.Indicator />
|
|
406
|
+
</RadioGroup.ItemControl>
|
|
407
|
+
<RadioGroup.ItemText>Español</RadioGroup.ItemText>
|
|
408
|
+
<RadioGroup.ItemHiddenInput />
|
|
409
|
+
</RadioGroup.Item>
|
|
410
|
+
<RadioGroup.Item value="fr">
|
|
411
|
+
<RadioGroup.ItemControl>
|
|
412
|
+
<RadioGroup.Indicator />
|
|
413
|
+
</RadioGroup.ItemControl>
|
|
414
|
+
<RadioGroup.ItemText>Français</RadioGroup.ItemText>
|
|
415
|
+
<RadioGroup.ItemHiddenInput />
|
|
416
|
+
</RadioGroup.Item>
|
|
417
|
+
</RadioGroup.Root>
|
|
418
|
+
</div>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## DO NOT
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// ❌ Don't use for multiple selections (use Checkbox instead)
|
|
425
|
+
<RadioGroup.Root>
|
|
426
|
+
<RadioGroup.Label>Select all that apply</RadioGroup.Label>
|
|
427
|
+
{/* Radio groups are for single selection only */}
|
|
428
|
+
</RadioGroup.Root>
|
|
429
|
+
|
|
430
|
+
// ✅ Use Checkbox for multiple selections
|
|
431
|
+
<CheckboxGroup>
|
|
432
|
+
<Checkbox value="option1">Option 1</Checkbox>
|
|
433
|
+
<Checkbox value="option2">Option 2</Checkbox>
|
|
434
|
+
</CheckboxGroup>
|
|
435
|
+
|
|
436
|
+
// ❌ Don't use for many options (>7 items, use Select instead)
|
|
437
|
+
<RadioGroup.Root>
|
|
438
|
+
<RadioGroup.Item value="country1">Country 1</RadioGroup.Item>
|
|
439
|
+
<RadioGroup.Item value="country2">Country 2</RadioGroup.Item>
|
|
440
|
+
{/* ... 50+ more countries */}
|
|
441
|
+
</RadioGroup.Root>
|
|
442
|
+
|
|
443
|
+
// ✅ Use Select for many options
|
|
444
|
+
<Select.Root>
|
|
445
|
+
<Select.Trigger>
|
|
446
|
+
<Select.ValueText placeholder="Select country" />
|
|
447
|
+
</Select.Trigger>
|
|
448
|
+
<Select.Content>
|
|
449
|
+
{countries.map((country) => (
|
|
450
|
+
<Select.Item key={country.value} item={country.value}>
|
|
451
|
+
<Select.ItemText>{country.label}</Select.ItemText>
|
|
452
|
+
</Select.Item>
|
|
453
|
+
))}
|
|
454
|
+
</Select.Content>
|
|
455
|
+
</Select.Root>
|
|
456
|
+
|
|
457
|
+
// ❌ Don't omit RadioGroup.ItemHiddenInput (breaks form submission)
|
|
458
|
+
<RadioGroup.Item value="option1">
|
|
459
|
+
<RadioGroup.ItemControl>
|
|
460
|
+
<RadioGroup.Indicator />
|
|
461
|
+
</RadioGroup.ItemControl>
|
|
462
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
463
|
+
{/* Missing ItemHiddenInput */}
|
|
464
|
+
</RadioGroup.Item>
|
|
465
|
+
|
|
466
|
+
// ✅ Always include ItemHiddenInput
|
|
467
|
+
<RadioGroup.Item value="option1">
|
|
468
|
+
<RadioGroup.ItemControl>
|
|
469
|
+
<RadioGroup.Indicator />
|
|
470
|
+
</RadioGroup.ItemControl>
|
|
471
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
472
|
+
<RadioGroup.ItemHiddenInput />
|
|
473
|
+
</RadioGroup.Item>
|
|
474
|
+
|
|
475
|
+
// ❌ Don't use for binary choices that are actions (use Switch instead)
|
|
476
|
+
<RadioGroup.Root>
|
|
477
|
+
<RadioGroup.Item value="enabled">Enable notifications</RadioGroup.Item>
|
|
478
|
+
<RadioGroup.Item value="disabled">Disable notifications</RadioGroup.Item>
|
|
479
|
+
</RadioGroup.Root>
|
|
480
|
+
|
|
481
|
+
// ✅ Use Switch for on/off toggles
|
|
482
|
+
<Switch.Root>
|
|
483
|
+
<Switch.Label>Enable notifications</Switch.Label>
|
|
484
|
+
<Switch.Control>
|
|
485
|
+
<Switch.Thumb />
|
|
486
|
+
</Switch.Control>
|
|
487
|
+
</Switch.Root>
|
|
488
|
+
|
|
489
|
+
// ❌ Don't create radio groups without a label
|
|
490
|
+
<RadioGroup.Root>
|
|
491
|
+
{/* No label - unclear what user is choosing */}
|
|
492
|
+
<RadioGroup.Item value="option1">...</RadioGroup.Item>
|
|
493
|
+
</RadioGroup.Root>
|
|
494
|
+
|
|
495
|
+
// ✅ Always provide a clear group label
|
|
496
|
+
<RadioGroup.Root>
|
|
497
|
+
<RadioGroup.Label>Select your preference</RadioGroup.Label>
|
|
498
|
+
<RadioGroup.Item value="option1">...</RadioGroup.Item>
|
|
499
|
+
</RadioGroup.Root>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Accessibility
|
|
503
|
+
|
|
504
|
+
The RadioGroup component follows WCAG 2.1 Level AA standards:
|
|
505
|
+
|
|
506
|
+
- **Keyboard Navigation**:
|
|
507
|
+
- `Tab` moves focus to the selected radio or first radio if none selected
|
|
508
|
+
- Arrow keys (↑/↓ for vertical, ←/→ for horizontal) navigate between options
|
|
509
|
+
- `Space` selects the focused radio
|
|
510
|
+
- **Focus Management**: Clear focus indicator on ItemControl with 2px outline
|
|
511
|
+
- **Screen Readers**:
|
|
512
|
+
- Group labeled with `role="radiogroup"` and `aria-labelledby`
|
|
513
|
+
- Each radio has `role="radio"` and `aria-checked` state
|
|
514
|
+
- Hidden input ensures form submission works correctly
|
|
515
|
+
- **Disabled State**:
|
|
516
|
+
- Uses `aria-disabled` attribute
|
|
517
|
+
- Visual opacity reduction (layerStyle: 'disabled')
|
|
518
|
+
- Prevents interaction while maintaining focusability for context
|
|
519
|
+
- **Required Fields**: Use `aria-required` on Root for required groups
|
|
520
|
+
|
|
521
|
+
### Accessibility Best Practices
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// ✅ Always provide a descriptive group label
|
|
525
|
+
<RadioGroup.Root>
|
|
526
|
+
<RadioGroup.Label>Select shipping method</RadioGroup.Label>
|
|
527
|
+
{/* options */}
|
|
528
|
+
</RadioGroup.Root>
|
|
529
|
+
|
|
530
|
+
// ✅ Provide helpful descriptions for complex options
|
|
531
|
+
<RadioGroup.Item value="express">
|
|
532
|
+
<RadioGroup.ItemControl>
|
|
533
|
+
<RadioGroup.Indicator />
|
|
534
|
+
</RadioGroup.ItemControl>
|
|
535
|
+
<RadioGroup.ItemText>
|
|
536
|
+
<span>Express Shipping</span>
|
|
537
|
+
<span className={css({ fontSize: 'sm', color: 'fg.subtle' })}>
|
|
538
|
+
2-3 business days, $15.99
|
|
539
|
+
</span>
|
|
540
|
+
</RadioGroup.ItemText>
|
|
541
|
+
<RadioGroup.ItemHiddenInput />
|
|
542
|
+
</RadioGroup.Item>
|
|
543
|
+
|
|
544
|
+
// ✅ Mark required fields clearly
|
|
545
|
+
<RadioGroup.Root aria-required="true">
|
|
546
|
+
<RadioGroup.Label>
|
|
547
|
+
Payment method
|
|
548
|
+
<span className={css({ color: 'error.fg' })}> *</span>
|
|
549
|
+
</RadioGroup.Label>
|
|
550
|
+
{/* options */}
|
|
551
|
+
</RadioGroup.Root>
|
|
552
|
+
|
|
553
|
+
// ✅ Use appropriate orientation for context
|
|
554
|
+
<RadioGroup.Root
|
|
555
|
+
orientation="horizontal" // Good for simple yes/no
|
|
556
|
+
defaultValue="yes"
|
|
557
|
+
>
|
|
558
|
+
<RadioGroup.Label>Accept terms?</RadioGroup.Label>
|
|
559
|
+
<RadioGroup.Item value="yes">Yes</RadioGroup.Item>
|
|
560
|
+
<RadioGroup.Item value="no">No</RadioGroup.Item>
|
|
561
|
+
</RadioGroup.Root>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
## Orientation Selection Guide
|
|
565
|
+
|
|
566
|
+
| Scenario | Recommended Orientation | Reasoning |
|
|
567
|
+
| ------------------------- | ----------------------- | ----------------------------------------- |
|
|
568
|
+
| 2-3 simple options | `horizontal` | Saves vertical space, easy to scan |
|
|
569
|
+
| 4+ options | `vertical` | Easier to read and compare options |
|
|
570
|
+
| Options with descriptions | `vertical` | Provides room for additional text |
|
|
571
|
+
| Toolbar/filter settings | `horizontal` | Fits naturally in horizontal UI |
|
|
572
|
+
| Form fields | `vertical` | Consistent with standard form layouts |
|
|
573
|
+
| Yes/No or binary choices | `horizontal` | Emphasizes the choice between two options |
|
|
574
|
+
|
|
575
|
+
## Size Selection Guide
|
|
576
|
+
|
|
577
|
+
| Scenario | Recommended Size | Reasoning |
|
|
578
|
+
| -------------------- | ---------------- | ----------------------------------- |
|
|
579
|
+
| Mobile interfaces | `lg` | Larger touch targets (44px minimum) |
|
|
580
|
+
| Desktop forms | `md` | Standard, comfortable size |
|
|
581
|
+
| Dense layouts/tables | `sm` | Saves space while remaining usable |
|
|
582
|
+
| Settings panels | `md` or `lg` | Emphasizes important choices |
|
|
583
|
+
| Inline options | `sm` or `md` | Fits naturally in content flow |
|
|
584
|
+
|
|
585
|
+
## State Behaviors
|
|
586
|
+
|
|
587
|
+
| State | Visual Change | Behavior |
|
|
588
|
+
| ---------------------- | ---------------------------------------- | ---------------------------------------- |
|
|
589
|
+
| **Default** | Gray border circle, white background | Clickable, focusable |
|
|
590
|
+
| **Hover** | Subtle background change on item | Indicates interactivity |
|
|
591
|
+
| **Checked** | Colored background, white inner dot | Shows selection, maintains focus ring |
|
|
592
|
+
| **Focus** | 2px outline ring on control | Keyboard navigation indicator |
|
|
593
|
+
| **Disabled** | Reduced opacity, gray appearance | Cannot be interacted with, not focusable |
|
|
594
|
+
| **Disabled + Checked** | Reduced opacity, maintains checked state | Shows selected but unavailable option |
|
|
595
|
+
|
|
596
|
+
## When to Use RadioGroup vs. Other Components
|
|
597
|
+
|
|
598
|
+
| Use RadioGroup When | Use Instead |
|
|
599
|
+
| ---------------------------------------------------- | ------------------------------------------------------ |
|
|
600
|
+
| User must choose exactly one option from 2-7 choices | - |
|
|
601
|
+
| All options should be visible at once | Use **Select** if 8+ options or limited space |
|
|
602
|
+
| Selection is a primary action | - |
|
|
603
|
+
| Options need to be compared | - |
|
|
604
|
+
| User needs to choose multiple items | Use **Checkbox** for multi-select |
|
|
605
|
+
| Binary on/off toggle for immediate action | Use **Switch** for instant state changes |
|
|
606
|
+
| Navigation between views/tabs | Use **Tabs** for content switching |
|
|
607
|
+
| Filtering data | Use **Select** or **Checkbox** based on space/quantity |
|
|
608
|
+
|
|
609
|
+
## Responsive Considerations
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
// Mobile-first: Use larger size and vertical orientation
|
|
613
|
+
<RadioGroup.Root
|
|
614
|
+
size="lg"
|
|
615
|
+
orientation="vertical"
|
|
616
|
+
defaultValue="option1"
|
|
617
|
+
>
|
|
618
|
+
<RadioGroup.Label>Select option</RadioGroup.Label>
|
|
619
|
+
{/* options */}
|
|
620
|
+
</RadioGroup.Root>
|
|
621
|
+
|
|
622
|
+
// Responsive orientation
|
|
623
|
+
<RadioGroup.Root
|
|
624
|
+
orientation={{ base: 'vertical', md: 'horizontal' }}
|
|
625
|
+
size={{ base: 'lg', md: 'md' }}
|
|
626
|
+
defaultValue="option1"
|
|
627
|
+
>
|
|
628
|
+
<RadioGroup.Label>Delivery preference</RadioGroup.Label>
|
|
629
|
+
{/* options */}
|
|
630
|
+
</RadioGroup.Root>
|
|
631
|
+
|
|
632
|
+
// Responsive container for groups
|
|
633
|
+
<div className={css({
|
|
634
|
+
display: 'grid',
|
|
635
|
+
gridTemplateColumns: { base: '1fr', md: '1fr 1fr' },
|
|
636
|
+
gap: '6'
|
|
637
|
+
})}>
|
|
638
|
+
<RadioGroup.Root defaultValue="option1">
|
|
639
|
+
<RadioGroup.Label>Category 1</RadioGroup.Label>
|
|
640
|
+
{/* options */}
|
|
641
|
+
</RadioGroup.Root>
|
|
642
|
+
<RadioGroup.Root defaultValue="option2">
|
|
643
|
+
<RadioGroup.Label>Category 2</RadioGroup.Label>
|
|
644
|
+
{/* options */}
|
|
645
|
+
</RadioGroup.Root>
|
|
646
|
+
</div>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Testing
|
|
650
|
+
|
|
651
|
+
When testing RadioGroup components:
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { render, screen } from '@testing-library/react';
|
|
655
|
+
import userEvent from '@testing-library/user-event';
|
|
656
|
+
|
|
657
|
+
test('selects radio option on click', async () => {
|
|
658
|
+
const handleChange = vi.fn();
|
|
659
|
+
render(
|
|
660
|
+
<RadioGroup.Root onValueChange={handleChange}>
|
|
661
|
+
<RadioGroup.Label>Choose option</RadioGroup.Label>
|
|
662
|
+
<RadioGroup.Item value="option1">
|
|
663
|
+
<RadioGroup.ItemControl>
|
|
664
|
+
<RadioGroup.Indicator />
|
|
665
|
+
</RadioGroup.ItemControl>
|
|
666
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
667
|
+
<RadioGroup.ItemHiddenInput />
|
|
668
|
+
</RadioGroup.Item>
|
|
669
|
+
<RadioGroup.Item value="option2">
|
|
670
|
+
<RadioGroup.ItemControl>
|
|
671
|
+
<RadioGroup.Indicator />
|
|
672
|
+
</RadioGroup.ItemControl>
|
|
673
|
+
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
|
|
674
|
+
<RadioGroup.ItemHiddenInput />
|
|
675
|
+
</RadioGroup.Item>
|
|
676
|
+
</RadioGroup.Root>
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const option2 = screen.getByText('Option 2');
|
|
680
|
+
await userEvent.click(option2);
|
|
681
|
+
|
|
682
|
+
expect(handleChange).toHaveBeenCalledWith(
|
|
683
|
+
expect.objectContaining({ value: 'option2' })
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test('keyboard navigation works correctly', async () => {
|
|
688
|
+
const user = userEvent.setup();
|
|
689
|
+
render(
|
|
690
|
+
<RadioGroup.Root defaultValue="option1">
|
|
691
|
+
<RadioGroup.Label>Choose option</RadioGroup.Label>
|
|
692
|
+
<RadioGroup.Item value="option1">
|
|
693
|
+
<RadioGroup.ItemControl>
|
|
694
|
+
<RadioGroup.Indicator />
|
|
695
|
+
</RadioGroup.ItemControl>
|
|
696
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
697
|
+
<RadioGroup.ItemHiddenInput />
|
|
698
|
+
</RadioGroup.Item>
|
|
699
|
+
<RadioGroup.Item value="option2">
|
|
700
|
+
<RadioGroup.ItemControl>
|
|
701
|
+
<RadioGroup.Indicator />
|
|
702
|
+
</RadioGroup.ItemControl>
|
|
703
|
+
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
|
|
704
|
+
<RadioGroup.ItemHiddenInput />
|
|
705
|
+
</RadioGroup.Item>
|
|
706
|
+
</RadioGroup.Root>
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const radioGroup = screen.getByRole('radiogroup');
|
|
710
|
+
await user.tab(); // Focus first radio
|
|
711
|
+
|
|
712
|
+
const option1 = screen.getByRole('radio', { name: 'Option 1' });
|
|
713
|
+
expect(option1).toHaveFocus();
|
|
714
|
+
|
|
715
|
+
await user.keyboard('{ArrowDown}');
|
|
716
|
+
const option2 = screen.getByRole('radio', { name: 'Option 2' });
|
|
717
|
+
expect(option2).toHaveFocus();
|
|
718
|
+
expect(option2).toBeChecked();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('disabled radio cannot be selected', async () => {
|
|
722
|
+
const handleChange = vi.fn();
|
|
723
|
+
render(
|
|
724
|
+
<RadioGroup.Root onValueChange={handleChange}>
|
|
725
|
+
<RadioGroup.Label>Choose option</RadioGroup.Label>
|
|
726
|
+
<RadioGroup.Item value="option1">
|
|
727
|
+
<RadioGroup.ItemControl>
|
|
728
|
+
<RadioGroup.Indicator />
|
|
729
|
+
</RadioGroup.ItemControl>
|
|
730
|
+
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
|
|
731
|
+
<RadioGroup.ItemHiddenInput />
|
|
732
|
+
</RadioGroup.Item>
|
|
733
|
+
<RadioGroup.Item value="option2" disabled>
|
|
734
|
+
<RadioGroup.ItemControl>
|
|
735
|
+
<RadioGroup.Indicator />
|
|
736
|
+
</RadioGroup.ItemControl>
|
|
737
|
+
<RadioGroup.ItemText>Option 2 (Disabled)</RadioGroup.ItemText>
|
|
738
|
+
<RadioGroup.ItemHiddenInput />
|
|
739
|
+
</RadioGroup.Item>
|
|
740
|
+
</RadioGroup.Root>
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const disabledOption = screen.getByRole('radio', { name: /Option 2/ });
|
|
744
|
+
await userEvent.click(disabledOption);
|
|
745
|
+
|
|
746
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
747
|
+
expect(disabledOption).not.toBeChecked();
|
|
748
|
+
});
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## Related Components
|
|
752
|
+
|
|
753
|
+
- **Checkbox**: For multiple selection scenarios
|
|
754
|
+
- **Switch**: For binary on/off toggles with immediate effect
|
|
755
|
+
- **Select**: For choosing from many options (8+) or when space is limited
|
|
756
|
+
- **Button**: For triggering actions or navigation
|
|
757
|
+
- **Tabs**: For switching between content views
|