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