@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,583 @@
|
|
|
1
|
+
# Checkbox
|
|
2
|
+
|
|
3
|
+
**Purpose:** Binary selection control for toggling options on/off, supporting single checkboxes and checkbox groups.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import * as Checkbox from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Component Structure
|
|
12
|
+
|
|
13
|
+
Checkbox uses a compound component pattern with these parts:
|
|
14
|
+
|
|
15
|
+
- `Checkbox.Root` - Container for the checkbox
|
|
16
|
+
- `Checkbox.Control` - The visual checkbox element
|
|
17
|
+
- `Checkbox.Label` - Text label for the checkbox
|
|
18
|
+
- `Checkbox.Indicator` - Checkmark icon (built-in)
|
|
19
|
+
- `Checkbox.HiddenInput` - Hidden native input
|
|
20
|
+
- `Checkbox.Group` - Container for multiple checkboxes
|
|
21
|
+
|
|
22
|
+
## Variants
|
|
23
|
+
|
|
24
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
25
|
+
| --------- | ------------------------------------- | -------------------- | ------------------------ |
|
|
26
|
+
| `solid` | Filled background when checked | Primary checkboxes | Default, most use cases |
|
|
27
|
+
| `outline` | Border only, highlighted when checked | Secondary checkboxes | Forms with less emphasis |
|
|
28
|
+
| `surface` | Surface background | Alternative style | Cards, elevated surfaces |
|
|
29
|
+
| `subtle` | Subtle background | Low-emphasis options | Settings, preferences |
|
|
30
|
+
| `plain` | Minimal styling | Text-like checkboxes | Inline selections |
|
|
31
|
+
|
|
32
|
+
### Visual Characteristics
|
|
33
|
+
|
|
34
|
+
- **solid**: Filled with primary color when checked, white checkmark
|
|
35
|
+
- **outline**: Border changes to primary color when checked, checkmark inside
|
|
36
|
+
- **surface**: Surface background with border
|
|
37
|
+
- **subtle**: Subtle gray background
|
|
38
|
+
- **plain**: Minimal styling, blends with text
|
|
39
|
+
|
|
40
|
+
## Sizes
|
|
41
|
+
|
|
42
|
+
| Size | Box Size | Label Text | Usage |
|
|
43
|
+
| ---- | -------- | ------------- | ---------------------------- |
|
|
44
|
+
| `sm` | 18px | Small (14px) | Compact forms, dense layouts |
|
|
45
|
+
| `md` | 20px | Medium (16px) | Default, most use cases |
|
|
46
|
+
| `lg` | 22px | Large (18px) | Touch targets, mobile-first |
|
|
47
|
+
|
|
48
|
+
**Recommendation:** Use `md` for most cases. Use `lg` for mobile or touch-focused interfaces.
|
|
49
|
+
|
|
50
|
+
## Props
|
|
51
|
+
|
|
52
|
+
### Root Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
| ----------------- | ---------------------------- | ------- | ----------------------------------- |
|
|
56
|
+
| `checked` | `boolean \| 'indeterminate'` | - | Controlled checked state |
|
|
57
|
+
| `defaultChecked` | `boolean` | `false` | Uncontrolled default checked state |
|
|
58
|
+
| `onCheckedChange` | `(details) => void` | - | Callback when checked state changes |
|
|
59
|
+
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
60
|
+
| `invalid` | `boolean` | `false` | Mark as invalid |
|
|
61
|
+
| `required` | `boolean` | `false` | Mark as required |
|
|
62
|
+
| `name` | `string` | - | Form field name |
|
|
63
|
+
| `value` | `string` | - | Form field value |
|
|
64
|
+
|
|
65
|
+
### Style Props
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| --------- | ---------------------------------------------------------- | --------- | -------------------- |
|
|
69
|
+
| `variant` | `'solid' \| 'outline' \| 'surface' \| 'subtle' \| 'plain'` | `'solid'` | Visual style variant |
|
|
70
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Checkbox size |
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Basic Usage
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import * as Checkbox from '@discourser/design-system';
|
|
78
|
+
|
|
79
|
+
// Single checkbox
|
|
80
|
+
<Checkbox.Root>
|
|
81
|
+
<Checkbox.HiddenInput />
|
|
82
|
+
<Checkbox.Control>
|
|
83
|
+
<Checkbox.Indicator />
|
|
84
|
+
</Checkbox.Control>
|
|
85
|
+
<Checkbox.Label>Accept terms and conditions</Checkbox.Label>
|
|
86
|
+
</Checkbox.Root>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Controlled Checkbox
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const [accepted, setAccepted] = useState(false);
|
|
93
|
+
|
|
94
|
+
<Checkbox.Root
|
|
95
|
+
checked={accepted}
|
|
96
|
+
onCheckedChange={(details) => setAccepted(details.checked)}
|
|
97
|
+
>
|
|
98
|
+
<Checkbox.HiddenInput />
|
|
99
|
+
<Checkbox.Control>
|
|
100
|
+
<Checkbox.Indicator />
|
|
101
|
+
</Checkbox.Control>
|
|
102
|
+
<Checkbox.Label>I agree to the terms</Checkbox.Label>
|
|
103
|
+
</Checkbox.Root>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Different Variants
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Solid (default)
|
|
110
|
+
<Checkbox.Root variant="solid">
|
|
111
|
+
<Checkbox.HiddenInput />
|
|
112
|
+
<Checkbox.Control>
|
|
113
|
+
<Checkbox.Indicator />
|
|
114
|
+
</Checkbox.Control>
|
|
115
|
+
<Checkbox.Label>Solid checkbox</Checkbox.Label>
|
|
116
|
+
</Checkbox.Root>
|
|
117
|
+
|
|
118
|
+
// Outline
|
|
119
|
+
<Checkbox.Root variant="outline">
|
|
120
|
+
<Checkbox.HiddenInput />
|
|
121
|
+
<Checkbox.Control>
|
|
122
|
+
<Checkbox.Indicator />
|
|
123
|
+
</Checkbox.Control>
|
|
124
|
+
<Checkbox.Label>Outline checkbox</Checkbox.Label>
|
|
125
|
+
</Checkbox.Root>
|
|
126
|
+
|
|
127
|
+
// Subtle
|
|
128
|
+
<Checkbox.Root variant="subtle">
|
|
129
|
+
<Checkbox.HiddenInput />
|
|
130
|
+
<Checkbox.Control>
|
|
131
|
+
<Checkbox.Indicator />
|
|
132
|
+
</Checkbox.Control>
|
|
133
|
+
<Checkbox.Label>Subtle checkbox</Checkbox.Label>
|
|
134
|
+
</Checkbox.Root>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Different Sizes
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Small
|
|
141
|
+
<Checkbox.Root size="sm">
|
|
142
|
+
<Checkbox.HiddenInput />
|
|
143
|
+
<Checkbox.Control>
|
|
144
|
+
<Checkbox.Indicator />
|
|
145
|
+
</Checkbox.Control>
|
|
146
|
+
<Checkbox.Label>Small checkbox</Checkbox.Label>
|
|
147
|
+
</Checkbox.Root>
|
|
148
|
+
|
|
149
|
+
// Medium (default)
|
|
150
|
+
<Checkbox.Root size="md">
|
|
151
|
+
<Checkbox.HiddenInput />
|
|
152
|
+
<Checkbox.Control>
|
|
153
|
+
<Checkbox.Indicator />
|
|
154
|
+
</Checkbox.Control>
|
|
155
|
+
<Checkbox.Label>Medium checkbox</Checkbox.Label>
|
|
156
|
+
</Checkbox.Root>
|
|
157
|
+
|
|
158
|
+
// Large
|
|
159
|
+
<Checkbox.Root size="lg">
|
|
160
|
+
<Checkbox.HiddenInput />
|
|
161
|
+
<Checkbox.Control>
|
|
162
|
+
<Checkbox.Indicator />
|
|
163
|
+
</Checkbox.Control>
|
|
164
|
+
<Checkbox.Label>Large checkbox</Checkbox.Label>
|
|
165
|
+
</Checkbox.Root>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Indeterminate State
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// "Select all" checkbox that shows indeterminate when some items selected
|
|
172
|
+
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
|
173
|
+
const allItems = ['item1', 'item2', 'item3'];
|
|
174
|
+
const allSelected = selectedItems.length === allItems.length;
|
|
175
|
+
const someSelected = selectedItems.length > 0 && !allSelected;
|
|
176
|
+
|
|
177
|
+
<Checkbox.Root
|
|
178
|
+
checked={someSelected ? 'indeterminate' : allSelected}
|
|
179
|
+
onCheckedChange={(details) => {
|
|
180
|
+
if (details.checked) {
|
|
181
|
+
setSelectedItems(allItems);
|
|
182
|
+
} else {
|
|
183
|
+
setSelectedItems([]);
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<Checkbox.HiddenInput />
|
|
188
|
+
<Checkbox.Control>
|
|
189
|
+
<Checkbox.Indicator />
|
|
190
|
+
</Checkbox.Control>
|
|
191
|
+
<Checkbox.Label>Select all</Checkbox.Label>
|
|
192
|
+
</Checkbox.Root>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Checkbox Group
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import * as Checkbox from '@discourser/design-system';
|
|
199
|
+
|
|
200
|
+
const [selectedFeatures, setSelectedFeatures] = useState<string[]>(['email']);
|
|
201
|
+
|
|
202
|
+
<Checkbox.Group
|
|
203
|
+
value={selectedFeatures}
|
|
204
|
+
onValueChange={(details) => setSelectedFeatures(details.value)}
|
|
205
|
+
>
|
|
206
|
+
<label>
|
|
207
|
+
<span>Notification Preferences</span>
|
|
208
|
+
</label>
|
|
209
|
+
|
|
210
|
+
<Checkbox.Root value="email">
|
|
211
|
+
<Checkbox.HiddenInput />
|
|
212
|
+
<Checkbox.Control>
|
|
213
|
+
<Checkbox.Indicator />
|
|
214
|
+
</Checkbox.Control>
|
|
215
|
+
<Checkbox.Label>Email notifications</Checkbox.Label>
|
|
216
|
+
</Checkbox.Root>
|
|
217
|
+
|
|
218
|
+
<Checkbox.Root value="push">
|
|
219
|
+
<Checkbox.HiddenInput />
|
|
220
|
+
<Checkbox.Control>
|
|
221
|
+
<Checkbox.Indicator />
|
|
222
|
+
</Checkbox.Control>
|
|
223
|
+
<Checkbox.Label>Push notifications</Checkbox.Label>
|
|
224
|
+
</Checkbox.Root>
|
|
225
|
+
|
|
226
|
+
<Checkbox.Root value="sms">
|
|
227
|
+
<Checkbox.HiddenInput />
|
|
228
|
+
<Checkbox.Control>
|
|
229
|
+
<Checkbox.Indicator />
|
|
230
|
+
</Checkbox.Control>
|
|
231
|
+
<Checkbox.Label>SMS notifications</Checkbox.Label>
|
|
232
|
+
</Checkbox.Root>
|
|
233
|
+
</Checkbox.Group>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Disabled State
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Disabled unchecked
|
|
240
|
+
<Checkbox.Root disabled>
|
|
241
|
+
<Checkbox.HiddenInput />
|
|
242
|
+
<Checkbox.Control>
|
|
243
|
+
<Checkbox.Indicator />
|
|
244
|
+
</Checkbox.Control>
|
|
245
|
+
<Checkbox.Label>Disabled option</Checkbox.Label>
|
|
246
|
+
</Checkbox.Root>
|
|
247
|
+
|
|
248
|
+
// Disabled checked
|
|
249
|
+
<Checkbox.Root disabled checked>
|
|
250
|
+
<Checkbox.HiddenInput />
|
|
251
|
+
<Checkbox.Control>
|
|
252
|
+
<Checkbox.Indicator />
|
|
253
|
+
</Checkbox.Control>
|
|
254
|
+
<Checkbox.Label>Disabled checked option</Checkbox.Label>
|
|
255
|
+
</Checkbox.Root>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Invalid State
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const [accepted, setAccepted] = useState(false);
|
|
262
|
+
const [submitted, setSubmitted] = useState(false);
|
|
263
|
+
const invalid = submitted && !accepted;
|
|
264
|
+
|
|
265
|
+
<div>
|
|
266
|
+
<Checkbox.Root
|
|
267
|
+
checked={accepted}
|
|
268
|
+
invalid={invalid}
|
|
269
|
+
onCheckedChange={(details) => setAccepted(details.checked)}
|
|
270
|
+
>
|
|
271
|
+
<Checkbox.HiddenInput />
|
|
272
|
+
<Checkbox.Control>
|
|
273
|
+
<Checkbox.Indicator />
|
|
274
|
+
</Checkbox.Control>
|
|
275
|
+
<Checkbox.Label>I accept the terms and conditions *</Checkbox.Label>
|
|
276
|
+
</Checkbox.Root>
|
|
277
|
+
|
|
278
|
+
{invalid && (
|
|
279
|
+
<div className={css({ color: 'error', textStyle: 'sm', mt: '1' })}>
|
|
280
|
+
You must accept the terms to continue
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
<Button onClick={() => setSubmitted(true)}>Submit</Button>
|
|
285
|
+
</div>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Common Patterns
|
|
289
|
+
|
|
290
|
+
### Terms and Conditions
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const [agreed, setAgreed] = useState(false);
|
|
294
|
+
|
|
295
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
|
|
296
|
+
<Checkbox.Root
|
|
297
|
+
checked={agreed}
|
|
298
|
+
onCheckedChange={(details) => setAgreed(details.checked)}
|
|
299
|
+
>
|
|
300
|
+
<Checkbox.HiddenInput />
|
|
301
|
+
<Checkbox.Control>
|
|
302
|
+
<Checkbox.Indicator />
|
|
303
|
+
</Checkbox.Control>
|
|
304
|
+
<Checkbox.Label>
|
|
305
|
+
I agree to the{' '}
|
|
306
|
+
<a href="/terms" className={css({ color: 'primary', textDecoration: 'underline' })}>
|
|
307
|
+
Terms of Service
|
|
308
|
+
</a>{' '}
|
|
309
|
+
and{' '}
|
|
310
|
+
<a href="/privacy" className={css({ color: 'primary', textDecoration: 'underline' })}>
|
|
311
|
+
Privacy Policy
|
|
312
|
+
</a>
|
|
313
|
+
</Checkbox.Label>
|
|
314
|
+
</Checkbox.Root>
|
|
315
|
+
|
|
316
|
+
<Button variant="filled" disabled={!agreed}>
|
|
317
|
+
Continue
|
|
318
|
+
</Button>
|
|
319
|
+
</div>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Settings List
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const [settings, setSettings] = useState({
|
|
326
|
+
emailNotifications: true,
|
|
327
|
+
pushNotifications: false,
|
|
328
|
+
darkMode: true,
|
|
329
|
+
autoSave: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'lg' })}>
|
|
333
|
+
<Heading size="md">Settings</Heading>
|
|
334
|
+
|
|
335
|
+
<Checkbox.Root
|
|
336
|
+
checked={settings.emailNotifications}
|
|
337
|
+
onCheckedChange={(details) =>
|
|
338
|
+
setSettings({ ...settings, emailNotifications: details.checked })
|
|
339
|
+
}
|
|
340
|
+
>
|
|
341
|
+
<Checkbox.HiddenInput />
|
|
342
|
+
<Checkbox.Control>
|
|
343
|
+
<Checkbox.Indicator />
|
|
344
|
+
</Checkbox.Control>
|
|
345
|
+
<Checkbox.Label>Email notifications</Checkbox.Label>
|
|
346
|
+
</Checkbox.Root>
|
|
347
|
+
|
|
348
|
+
<Checkbox.Root
|
|
349
|
+
checked={settings.pushNotifications}
|
|
350
|
+
onCheckedChange={(details) =>
|
|
351
|
+
setSettings({ ...settings, pushNotifications: details.checked })
|
|
352
|
+
}
|
|
353
|
+
>
|
|
354
|
+
<Checkbox.HiddenInput />
|
|
355
|
+
<Checkbox.Control>
|
|
356
|
+
<Checkbox.Indicator />
|
|
357
|
+
</Checkbox.Control>
|
|
358
|
+
<Checkbox.Label>Push notifications</Checkbox.Label>
|
|
359
|
+
</Checkbox.Root>
|
|
360
|
+
|
|
361
|
+
<Checkbox.Root
|
|
362
|
+
checked={settings.darkMode}
|
|
363
|
+
onCheckedChange={(details) =>
|
|
364
|
+
setSettings({ ...settings, darkMode: details.checked })
|
|
365
|
+
}
|
|
366
|
+
>
|
|
367
|
+
<Checkbox.HiddenInput />
|
|
368
|
+
<Checkbox.Control>
|
|
369
|
+
<Checkbox.Indicator />
|
|
370
|
+
</Checkbox.Control>
|
|
371
|
+
<Checkbox.Label>Dark mode</Checkbox.Label>
|
|
372
|
+
</Checkbox.Root>
|
|
373
|
+
</div>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Filter List
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
|
|
380
|
+
|
|
381
|
+
<div>
|
|
382
|
+
<Heading size="sm">Filter Results</Heading>
|
|
383
|
+
|
|
384
|
+
<Checkbox.Group
|
|
385
|
+
value={selectedFilters}
|
|
386
|
+
onValueChange={(details) => setSelectedFilters(details.value)}
|
|
387
|
+
>
|
|
388
|
+
<Checkbox.Root value="inStock" variant="outline">
|
|
389
|
+
<Checkbox.HiddenInput />
|
|
390
|
+
<Checkbox.Control>
|
|
391
|
+
<Checkbox.Indicator />
|
|
392
|
+
</Checkbox.Control>
|
|
393
|
+
<Checkbox.Label>In stock</Checkbox.Label>
|
|
394
|
+
</Checkbox.Root>
|
|
395
|
+
|
|
396
|
+
<Checkbox.Root value="onSale" variant="outline">
|
|
397
|
+
<Checkbox.HiddenInput />
|
|
398
|
+
<Checkbox.Control>
|
|
399
|
+
<Checkbox.Indicator />
|
|
400
|
+
</Checkbox.Control>
|
|
401
|
+
<Checkbox.Label>On sale</Checkbox.Label>
|
|
402
|
+
</Checkbox.Root>
|
|
403
|
+
|
|
404
|
+
<Checkbox.Root value="freeShipping" variant="outline">
|
|
405
|
+
<Checkbox.HiddenInput />
|
|
406
|
+
<Checkbox.Control>
|
|
407
|
+
<Checkbox.Indicator />
|
|
408
|
+
</Checkbox.Control>
|
|
409
|
+
<Checkbox.Label>Free shipping</Checkbox.Label>
|
|
410
|
+
</Checkbox.Root>
|
|
411
|
+
</Checkbox.Group>
|
|
412
|
+
</div>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## DO NOT
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// ❌ Don't use native checkbox input
|
|
419
|
+
<input type="checkbox" /> // Use Checkbox component instead
|
|
420
|
+
|
|
421
|
+
// ❌ Don't forget HiddenInput (needed for forms)
|
|
422
|
+
<Checkbox.Root>
|
|
423
|
+
<Checkbox.Control>
|
|
424
|
+
<Checkbox.Indicator />
|
|
425
|
+
</Checkbox.Control>
|
|
426
|
+
<Checkbox.Label>Option</Checkbox.Label>
|
|
427
|
+
</Checkbox.Root> // Missing <Checkbox.HiddenInput />
|
|
428
|
+
|
|
429
|
+
// ❌ Don't forget Label (accessibility issue)
|
|
430
|
+
<Checkbox.Root>
|
|
431
|
+
<Checkbox.HiddenInput />
|
|
432
|
+
<Checkbox.Control>
|
|
433
|
+
<Checkbox.Indicator />
|
|
434
|
+
</Checkbox.Control>
|
|
435
|
+
</Checkbox.Root> // Missing label
|
|
436
|
+
|
|
437
|
+
// ❌ Don't use checkbox for single exclusive choice
|
|
438
|
+
<Checkbox.Root>...</Checkbox.Root> // Use RadioGroup for exclusive selections
|
|
439
|
+
|
|
440
|
+
// ❌ Don't override checkbox colors with inline styles
|
|
441
|
+
<Checkbox.Control style={{ backgroundColor: 'red' }}>
|
|
442
|
+
<Checkbox.Indicator />
|
|
443
|
+
</Checkbox.Control> // Use variants instead
|
|
444
|
+
|
|
445
|
+
// ✅ Use complete Checkbox structure
|
|
446
|
+
<Checkbox.Root>
|
|
447
|
+
<Checkbox.HiddenInput />
|
|
448
|
+
<Checkbox.Control>
|
|
449
|
+
<Checkbox.Indicator />
|
|
450
|
+
</Checkbox.Control>
|
|
451
|
+
<Checkbox.Label>Accept terms</Checkbox.Label>
|
|
452
|
+
</Checkbox.Root>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Accessibility
|
|
456
|
+
|
|
457
|
+
The Checkbox component follows WCAG 2.1 Level AA standards:
|
|
458
|
+
|
|
459
|
+
- **Keyboard Navigation**: Space to toggle, Tab to navigate
|
|
460
|
+
- **Focus Indicator**: Clear focus ring on keyboard navigation
|
|
461
|
+
- **ARIA Attributes**: Proper `role="checkbox"` and `aria-checked`
|
|
462
|
+
- **Labels**: Associated labels for screen readers
|
|
463
|
+
- **Disabled State**: Proper `aria-disabled` attribute
|
|
464
|
+
- **Touch Targets**: Minimum 44x44px with size md or larger
|
|
465
|
+
|
|
466
|
+
### Accessibility Best Practices
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// ✅ Always provide a label
|
|
470
|
+
<Checkbox.Root>
|
|
471
|
+
<Checkbox.HiddenInput />
|
|
472
|
+
<Checkbox.Control>
|
|
473
|
+
<Checkbox.Indicator />
|
|
474
|
+
</Checkbox.Control>
|
|
475
|
+
<Checkbox.Label>Subscribe to newsletter</Checkbox.Label>
|
|
476
|
+
</Checkbox.Root>
|
|
477
|
+
|
|
478
|
+
// ✅ Use required attribute for required fields
|
|
479
|
+
<Checkbox.Root required>
|
|
480
|
+
<Checkbox.HiddenInput />
|
|
481
|
+
<Checkbox.Control>
|
|
482
|
+
<Checkbox.Indicator />
|
|
483
|
+
</Checkbox.Control>
|
|
484
|
+
<Checkbox.Label>I accept the terms *</Checkbox.Label>
|
|
485
|
+
</Checkbox.Root>
|
|
486
|
+
|
|
487
|
+
// ✅ Provide error messages for invalid state
|
|
488
|
+
<div>
|
|
489
|
+
<Checkbox.Root invalid={!accepted && submitted}>
|
|
490
|
+
<Checkbox.HiddenInput />
|
|
491
|
+
<Checkbox.Control>
|
|
492
|
+
<Checkbox.Indicator />
|
|
493
|
+
</Checkbox.Control>
|
|
494
|
+
<Checkbox.Label>Accept terms</Checkbox.Label>
|
|
495
|
+
</Checkbox.Root>
|
|
496
|
+
{!accepted && submitted && (
|
|
497
|
+
<span role="alert" className={css({ color: 'error' })}>
|
|
498
|
+
Required field
|
|
499
|
+
</span>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
// ✅ Group related checkboxes
|
|
504
|
+
<Checkbox.Group aria-label="Notification preferences">
|
|
505
|
+
{/* Checkboxes here */}
|
|
506
|
+
</Checkbox.Group>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Variant Selection Guide
|
|
510
|
+
|
|
511
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
512
|
+
| ----------------- | ------------------- | -------------------------------------- |
|
|
513
|
+
| Terms acceptance | `solid` | Clear, prominent for important consent |
|
|
514
|
+
| Settings toggles | `solid` or `subtle` | Clear state indication |
|
|
515
|
+
| Filter options | `outline` | Less prominent, multiple selections |
|
|
516
|
+
| Feature flags | `solid` | Default, clear on/off state |
|
|
517
|
+
| Inline selections | `plain` | Minimal, blends with content |
|
|
518
|
+
| Form checkboxes | `solid` | Standard, clear visual feedback |
|
|
519
|
+
|
|
520
|
+
## State Behaviors
|
|
521
|
+
|
|
522
|
+
| State | Visual Change | Behavior |
|
|
523
|
+
| ----------------- | ----------------------------- | -------------------------------------- |
|
|
524
|
+
| **Unchecked** | Empty box with border | Default state |
|
|
525
|
+
| **Checked** | Filled box with checkmark | Selected state |
|
|
526
|
+
| **Indeterminate** | Filled box with dash | Partial selection (e.g., "select all") |
|
|
527
|
+
| **Hover** | Background color change | Interactive feedback |
|
|
528
|
+
| **Focus** | Focus ring appears | Keyboard navigation indicator |
|
|
529
|
+
| **Disabled** | Grayed out, reduced opacity | Cannot be toggled |
|
|
530
|
+
| **Invalid** | Error color border/background | Validation error |
|
|
531
|
+
|
|
532
|
+
## Testing
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { render, screen } from '@testing-library/react';
|
|
536
|
+
import userEvent from '@testing-library/user-event';
|
|
537
|
+
|
|
538
|
+
test('checkbox can be checked and unchecked', async () => {
|
|
539
|
+
const handleChange = vi.fn();
|
|
540
|
+
|
|
541
|
+
render(
|
|
542
|
+
<Checkbox.Root onCheckedChange={handleChange}>
|
|
543
|
+
<Checkbox.HiddenInput />
|
|
544
|
+
<Checkbox.Control>
|
|
545
|
+
<Checkbox.Indicator />
|
|
546
|
+
</Checkbox.Control>
|
|
547
|
+
<Checkbox.Label>Accept terms</Checkbox.Label>
|
|
548
|
+
</Checkbox.Root>
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' });
|
|
552
|
+
|
|
553
|
+
await userEvent.click(checkbox);
|
|
554
|
+
expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ checked: true }));
|
|
555
|
+
|
|
556
|
+
await userEvent.click(checkbox);
|
|
557
|
+
expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ checked: false }));
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('disabled checkbox cannot be toggled', async () => {
|
|
561
|
+
render(
|
|
562
|
+
<Checkbox.Root disabled>
|
|
563
|
+
<Checkbox.HiddenInput />
|
|
564
|
+
<Checkbox.Control>
|
|
565
|
+
<Checkbox.Indicator />
|
|
566
|
+
</Checkbox.Control>
|
|
567
|
+
<Checkbox.Label>Disabled</Checkbox.Label>
|
|
568
|
+
</Checkbox.Root>
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const checkbox = screen.getByRole('checkbox');
|
|
572
|
+
await userEvent.click(checkbox);
|
|
573
|
+
|
|
574
|
+
expect(checkbox).not.toBeChecked();
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Related Components
|
|
579
|
+
|
|
580
|
+
- **RadioGroup** - For exclusive single selections
|
|
581
|
+
- **Switch** - For on/off toggles (different visual metaphor)
|
|
582
|
+
- **Button** - For action triggers
|
|
583
|
+
- **Select** - For choosing from many options
|