@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.
Files changed (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +60 -8
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,911 @@
1
+ # Slider
2
+
3
+ **Purpose:** Range selection control for choosing numeric values or ranges along a continuous scale following Material Design 3 patterns.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use Slider when you need users to **select a value or range from a continuous spectrum** (volume, brightness, price ranges, ratings).
8
+
9
+ **Decision Tree:**
10
+
11
+ | Scenario | Use This | Why |
12
+ | ------------------------------------------------------------ | ------------------------------ | --------------------------------- |
13
+ | Select value from continuous range (0-100, 1-10) | Slider ✅ | Visual, intuitive for ranges |
14
+ | Adjust settings with immediate feedback (volume, brightness) | Slider ✅ | Visual representation of value |
15
+ | Filter by range (price: $0-$1000, dates) | Slider with multiple thumbs ✅ | Dual-ended range selection |
16
+ | Precise numeric input (age, quantity) | Input with type="number" | Keyboard entry is more precise |
17
+ | Select from discrete options (1, 2, 3, 4, 5) | RadioGroup or Select | Explicit, distinct choices |
18
+ | Binary choice (on/off, yes/no) | Switch | Visual metaphor for state |
19
+ | Large range with precision needed (thousands) | Input with type="number" | Slider imprecise for large ranges |
20
+
21
+ **Component Comparison:**
22
+
23
+ ```typescript
24
+ // ✅ Use Slider for continuous ranges
25
+ <Slider.Root min={0} max={100} defaultValue={[50]}>
26
+ <Slider.Label>Volume</Slider.Label>
27
+ <Slider.Control>
28
+ <Slider.Track>
29
+ <Slider.Range />
30
+ </Slider.Track>
31
+ <Slider.Thumb />
32
+ </Slider.Control>
33
+ <Slider.ValueText />
34
+ </Slider.Root>
35
+
36
+ // ✅ Use Slider for range selection (price filter)
37
+ <Slider.Root min={0} max={1000} defaultValue={[100, 500]}>
38
+ <Slider.Label>Price Range</Slider.Label>
39
+ <Slider.Control>
40
+ <Slider.Track>
41
+ <Slider.Range />
42
+ </Slider.Track>
43
+ <Slider.Thumbs>
44
+ {({ thumbs }) => thumbs.map((thumb) => (
45
+ <Slider.Thumb key={thumb} index={thumb} />
46
+ ))}
47
+ </Slider.Thumbs>
48
+ </Slider.Control>
49
+ <Slider.ValueText />
50
+ </Slider.Root>
51
+
52
+ // ❌ Don't use Slider for precise numeric input
53
+ <Slider.Root min={0} max={9999} defaultValue={[5432]}>
54
+ <Slider.Label>Enter exact amount</Slider.Label>
55
+ {/* ... */}
56
+ </Slider.Root> // Wrong - hard to select precise values
57
+
58
+ <Input label="Enter exact amount" type="number" /> // Correct
59
+
60
+ // ❌ Don't use Slider for discrete options
61
+ <Slider.Root min={1} max={5} step={1}>
62
+ <Slider.Label>Rating</Slider.Label>
63
+ {/* ... */}
64
+ </Slider.Root> // Wrong - use discrete selector
65
+
66
+ <RadioGroup.Root>
67
+ <RadioGroup.Label>Rating</RadioGroup.Label>
68
+ <RadioGroup.Item value="1"><RadioGroup.ItemText>1 Star</RadioGroup.ItemText></RadioGroup.Item>
69
+ <RadioGroup.Item value="2"><RadioGroup.ItemText>2 Stars</RadioGroup.ItemText></RadioGroup.Item>
70
+ {/* ... */}
71
+ </RadioGroup.Root> // Correct
72
+
73
+ // ❌ Don't use Slider for binary choices
74
+ <Slider.Root min={0} max={1} defaultValue={[1]}>
75
+ <Slider.Label>Enable notifications</Slider.Label>
76
+ {/* ... */}
77
+ </Slider.Root> // Wrong - binary state
78
+
79
+ <Switch.Root>
80
+ <Switch.Label>Enable notifications</Switch.Label>
81
+ <Switch.Control><Switch.Thumb /></Switch.Control>
82
+ </Switch.Root> // Correct
83
+ ```
84
+
85
+ ## Import
86
+
87
+ ```typescript
88
+ import * as Slider from '@discourser/design-system';
89
+ ```
90
+
91
+ ## Component Structure
92
+
93
+ Slider uses a compound component pattern from Ark UI with these parts:
94
+
95
+ - `Slider.Root` - Container with value and range configuration
96
+ - `Slider.Label` - Label for the slider
97
+ - `Slider.Control` - Interactive control area
98
+ - `Slider.Track` - Background track
99
+ - `Slider.Range` - Highlighted active range
100
+ - `Slider.Thumb` - Draggable handle (single thumb)
101
+ - `Slider.Thumbs` - Container for multiple thumbs (range slider)
102
+ - `Slider.ValueText` - Display of current value(s)
103
+ - `Slider.Marker` - Individual marker indicator
104
+ - `Slider.MarkerGroup` - Container for markers
105
+
106
+ ## Variants
107
+
108
+ | Variant | Visual Style | Usage | When to Use |
109
+ | --------- | ------------------------------- | ---------------- | ----------------------- |
110
+ | `outline` | Track with border, filled range | Standard sliders | Default, most use cases |
111
+
112
+ **Note:** Currently only `outline` variant is implemented. Additional variants can be added as needed.
113
+
114
+ ## Sizes
115
+
116
+ | Size | Thumb Size | Track Height | Usage |
117
+ | ---- | ---------- | ------------ | --------------------------- |
118
+ | `sm` | 20px | 8px | Compact UI, dense layouts |
119
+ | `md` | 20px | 8px | Default, most use cases |
120
+ | `lg` | 20px | 8px | Touch targets, mobile-first |
121
+
122
+ **Recommendation:** Use `md` for most cases. Sizes are consistent to maintain usability.
123
+
124
+ ## Props
125
+
126
+ ### Root Props
127
+
128
+ | Prop | Type | Default | Description |
129
+ | ------------------ | ---------------------------- | -------------- | ----------------------------- |
130
+ | `min` | `number` | `0` | Minimum value |
131
+ | `max` | `number` | `100` | Maximum value |
132
+ | `step` | `number` | `1` | Increment step |
133
+ | `value` | `number[]` | - | Controlled value(s) |
134
+ | `defaultValue` | `number[]` | - | Uncontrolled default value(s) |
135
+ | `onValueChange` | `(details) => void` | - | Callback when value changes |
136
+ | `onValueChangeEnd` | `(details) => void` | - | Callback when dragging ends |
137
+ | `disabled` | `boolean` | `false` | Disable slider interaction |
138
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Slider orientation |
139
+ | `name` | `string` | - | Form field name |
140
+ | `colorPalette` | `string` | - | Color palette for theming |
141
+
142
+ ### Style Props
143
+
144
+ | Prop | Type | Default | Description |
145
+ | --------- | ---------------------- | ----------- | -------------------- |
146
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Slider size |
147
+ | `variant` | `'outline'` | `'outline'` | Visual style variant |
148
+
149
+ ## Examples
150
+
151
+ ### Basic Single Slider
152
+
153
+ ```typescript
154
+ import * as Slider from '@discourser/design-system';
155
+
156
+ // Simple slider with default range (0-100)
157
+ <Slider.Root defaultValue={[50]}>
158
+ <Slider.Label>Volume</Slider.Label>
159
+ <Slider.Control>
160
+ <Slider.Track>
161
+ <Slider.Range />
162
+ </Slider.Track>
163
+ <Slider.Thumb />
164
+ </Slider.Control>
165
+ <Slider.ValueText />
166
+ </Slider.Root>
167
+ ```
168
+
169
+ ### Controlled Slider
170
+
171
+ ```typescript
172
+ const [volume, setVolume] = useState([50]);
173
+
174
+ <Slider.Root
175
+ value={volume}
176
+ onValueChange={(details) => setVolume(details.value)}
177
+ >
178
+ <Slider.Label>Volume</Slider.Label>
179
+ <Slider.Control>
180
+ <Slider.Track>
181
+ <Slider.Range />
182
+ </Slider.Track>
183
+ <Slider.Thumb />
184
+ </Slider.Control>
185
+ <Slider.ValueText />
186
+ </Slider.Root>
187
+
188
+ // Display value elsewhere
189
+ <p>Current volume: {volume[0]}%</p>
190
+ ```
191
+
192
+ ### Custom Range and Step
193
+
194
+ ```typescript
195
+ // Temperature slider (0-30°C, step 0.5)
196
+ <Slider.Root min={0} max={30} step={0.5} defaultValue={[21]}>
197
+ <Slider.Label>Temperature (°C)</Slider.Label>
198
+ <Slider.Control>
199
+ <Slider.Track>
200
+ <Slider.Range />
201
+ </Slider.Track>
202
+ <Slider.Thumb />
203
+ </Slider.Control>
204
+ <Slider.ValueText />
205
+ </Slider.Root>
206
+
207
+ // Rating slider (1-10, whole numbers)
208
+ <Slider.Root min={1} max={10} step={1} defaultValue={[7]}>
209
+ <Slider.Label>Rating</Slider.Label>
210
+ <Slider.Control>
211
+ <Slider.Track>
212
+ <Slider.Range />
213
+ </Slider.Track>
214
+ <Slider.Thumb />
215
+ </Slider.Control>
216
+ <Slider.ValueText />
217
+ </Slider.Root>
218
+ ```
219
+
220
+ ### Range Slider (Dual Thumbs)
221
+
222
+ ```typescript
223
+ // Price range filter
224
+ const [priceRange, setPriceRange] = useState([100, 500]);
225
+
226
+ <Slider.Root
227
+ min={0}
228
+ max={1000}
229
+ value={priceRange}
230
+ onValueChange={(details) => setPriceRange(details.value)}
231
+ >
232
+ <Slider.Label>Price Range</Slider.Label>
233
+ <Slider.Control>
234
+ <Slider.Track>
235
+ <Slider.Range />
236
+ </Slider.Track>
237
+ <Slider.Thumbs>
238
+ {({ thumbs }) => thumbs.map((thumb) => (
239
+ <Slider.Thumb key={thumb} index={thumb} />
240
+ ))}
241
+ </Slider.Thumbs>
242
+ </Slider.Control>
243
+ <Slider.ValueText />
244
+ </Slider.Root>
245
+
246
+ // Display formatted range
247
+ <p>Price: ${priceRange[0]} - ${priceRange[1]}</p>
248
+ ```
249
+
250
+ ### With Custom Value Display
251
+
252
+ ```typescript
253
+ const [brightness, setBrightness] = useState([75]);
254
+
255
+ <Slider.Root
256
+ min={0}
257
+ max={100}
258
+ value={brightness}
259
+ onValueChange={(details) => setBrightness(details.value)}
260
+ >
261
+ <div className={css({ display: 'flex', justifyContent: 'space-between', mb: 'xs' })}>
262
+ <Slider.Label>Brightness</Slider.Label>
263
+ <span className={css({ fontWeight: 'semibold' })}>{brightness[0]}%</span>
264
+ </div>
265
+ <Slider.Control>
266
+ <Slider.Track>
267
+ <Slider.Range />
268
+ </Slider.Track>
269
+ <Slider.Thumb />
270
+ </Slider.Control>
271
+ </Slider.Root>
272
+ ```
273
+
274
+ ### Different Sizes
275
+
276
+ ```typescript
277
+ // Small
278
+ <Slider.Root size="sm" defaultValue={[50]}>
279
+ <Slider.Label>Volume</Slider.Label>
280
+ <Slider.Control>
281
+ <Slider.Track>
282
+ <Slider.Range />
283
+ </Slider.Track>
284
+ <Slider.Thumb />
285
+ </Slider.Control>
286
+ </Slider.Root>
287
+
288
+ // Medium (default)
289
+ <Slider.Root size="md" defaultValue={[50]}>
290
+ <Slider.Label>Volume</Slider.Label>
291
+ <Slider.Control>
292
+ <Slider.Track>
293
+ <Slider.Range />
294
+ </Slider.Track>
295
+ <Slider.Thumb />
296
+ </Slider.Control>
297
+ </Slider.Root>
298
+
299
+ // Large
300
+ <Slider.Root size="lg" defaultValue={[50]}>
301
+ <Slider.Label>Volume</Slider.Label>
302
+ <Slider.Control>
303
+ <Slider.Track>
304
+ <Slider.Range />
305
+ </Slider.Track>
306
+ <Slider.Thumb />
307
+ </Slider.Control>
308
+ </Slider.Root>
309
+ ```
310
+
311
+ ### Vertical Orientation
312
+
313
+ ```typescript
314
+ <Slider.Root
315
+ orientation="vertical"
316
+ min={0}
317
+ max={100}
318
+ defaultValue={[50]}
319
+ className={css({ height: '200px' })}
320
+ >
321
+ <Slider.Label>Volume</Slider.Label>
322
+ <Slider.Control>
323
+ <Slider.Track>
324
+ <Slider.Range />
325
+ </Slider.Track>
326
+ <Slider.Thumb />
327
+ </Slider.Control>
328
+ <Slider.ValueText />
329
+ </Slider.Root>
330
+ ```
331
+
332
+ ### With Markers
333
+
334
+ ```typescript
335
+ <Slider.Root min={0} max={100} step={25} defaultValue={[50]}>
336
+ <Slider.Label>Progress</Slider.Label>
337
+ <Slider.Control>
338
+ <Slider.Track>
339
+ <Slider.Range />
340
+ </Slider.Track>
341
+ <Slider.Thumb />
342
+ <Slider.MarkerGroup>
343
+ <Slider.Marker value={0}>0%</Slider.Marker>
344
+ <Slider.Marker value={25}>25%</Slider.Marker>
345
+ <Slider.Marker value={50}>50%</Slider.Marker>
346
+ <Slider.Marker value={75}>75%</Slider.Marker>
347
+ <Slider.Marker value={100}>100%</Slider.Marker>
348
+ </Slider.MarkerGroup>
349
+ </Slider.Control>
350
+ </Slider.Root>
351
+ ```
352
+
353
+ ### With Color Palette
354
+
355
+ ```typescript
356
+ // Primary color
357
+ <Slider.Root defaultValue={[50]} colorPalette="primary">
358
+ <Slider.Label>Volume</Slider.Label>
359
+ <Slider.Control>
360
+ <Slider.Track>
361
+ <Slider.Range />
362
+ </Slider.Track>
363
+ <Slider.Thumb />
364
+ </Slider.Control>
365
+ </Slider.Root>
366
+
367
+ // Success color
368
+ <Slider.Root defaultValue={[75]} colorPalette="success">
369
+ <Slider.Label>Progress</Slider.Label>
370
+ <Slider.Control>
371
+ <Slider.Track>
372
+ <Slider.Range />
373
+ </Slider.Track>
374
+ <Slider.Thumb />
375
+ </Slider.Control>
376
+ </Slider.Root>
377
+
378
+ // Error color
379
+ <Slider.Root defaultValue={[25]} colorPalette="error">
380
+ <Slider.Label>Risk Level</Slider.Label>
381
+ <Slider.Control>
382
+ <Slider.Track>
383
+ <Slider.Range />
384
+ </Slider.Track>
385
+ <Slider.Thumb />
386
+ </Slider.Control>
387
+ </Slider.Root>
388
+ ```
389
+
390
+ ### Disabled State
391
+
392
+ ```typescript
393
+ <Slider.Root defaultValue={[50]} disabled>
394
+ <Slider.Label>Volume (Disabled)</Slider.Label>
395
+ <Slider.Control>
396
+ <Slider.Track>
397
+ <Slider.Range />
398
+ </Slider.Track>
399
+ <Slider.Thumb />
400
+ </Slider.Control>
401
+ <Slider.ValueText />
402
+ </Slider.Root>
403
+ ```
404
+
405
+ ### With onChange Callback
406
+
407
+ ```typescript
408
+ const [volume, setVolume] = useState([50]);
409
+
410
+ <Slider.Root
411
+ value={volume}
412
+ onValueChange={(details) => {
413
+ setVolume(details.value);
414
+ console.log('Value changed:', details.value);
415
+ }}
416
+ onValueChangeEnd={(details) => {
417
+ console.log('Final value:', details.value);
418
+ // Save to backend or apply changes
419
+ }}
420
+ >
421
+ <Slider.Label>Volume</Slider.Label>
422
+ <Slider.Control>
423
+ <Slider.Track>
424
+ <Slider.Range />
425
+ </Slider.Track>
426
+ <Slider.Thumb />
427
+ </Slider.Control>
428
+ <Slider.ValueText />
429
+ </Slider.Root>
430
+ ```
431
+
432
+ ## Common Patterns
433
+
434
+ ### Volume Control
435
+
436
+ ```typescript
437
+ const [volume, setVolume] = useState([70]);
438
+ const [isMuted, setIsMuted] = useState(false);
439
+
440
+ <div className={css({ display: 'flex', alignItems: 'center', gap: 'md' })}>
441
+ <IconButton
442
+ variant="ghost"
443
+ size="sm"
444
+ aria-label={isMuted ? 'Unmute' : 'Mute'}
445
+ onClick={() => setIsMuted(!isMuted)}
446
+ >
447
+ {isMuted ? <VolumeOffIcon /> : <VolumeIcon />}
448
+ </IconButton>
449
+
450
+ <Slider.Root
451
+ className={css({ flex: 1 })}
452
+ value={isMuted ? [0] : volume}
453
+ onValueChange={(details) => {
454
+ setVolume(details.value);
455
+ if (details.value[0] > 0) setIsMuted(false);
456
+ }}
457
+ disabled={isMuted}
458
+ >
459
+ <Slider.Control>
460
+ <Slider.Track>
461
+ <Slider.Range />
462
+ </Slider.Track>
463
+ <Slider.Thumb />
464
+ </Slider.Control>
465
+ </Slider.Root>
466
+
467
+ <span className={css({ minW: '12', textAlign: 'right' })}>
468
+ {isMuted ? '0' : volume[0]}%
469
+ </span>
470
+ </div>
471
+ ```
472
+
473
+ ### Price Range Filter
474
+
475
+ ```typescript
476
+ const [priceRange, setPriceRange] = useState([50, 500]);
477
+
478
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
479
+ <div className={css({ display: 'flex', justifyContent: 'space-between' })}>
480
+ <span className={css({ fontWeight: 'medium' })}>Price Range</span>
481
+ <span className={css({ color: 'fg.muted' })}>
482
+ ${priceRange[0]} - ${priceRange[1]}
483
+ </span>
484
+ </div>
485
+
486
+ <Slider.Root
487
+ min={0}
488
+ max={1000}
489
+ step={10}
490
+ value={priceRange}
491
+ onValueChange={(details) => setPriceRange(details.value)}
492
+ >
493
+ <Slider.Control>
494
+ <Slider.Track>
495
+ <Slider.Range />
496
+ </Slider.Track>
497
+ <Slider.Thumbs>
498
+ {({ thumbs }) => thumbs.map((thumb) => (
499
+ <Slider.Thumb key={thumb} index={thumb} />
500
+ ))}
501
+ </Slider.Thumbs>
502
+ </Slider.Control>
503
+ </Slider.Root>
504
+
505
+ <div className={css({ display: 'flex', gap: 'md' })}>
506
+ <Input
507
+ type="number"
508
+ value={priceRange[0]}
509
+ onChange={(e) => setPriceRange([Number(e.target.value), priceRange[1]])}
510
+ prefix="$"
511
+ size="sm"
512
+ />
513
+ <span className={css({ alignSelf: 'center' })}>to</span>
514
+ <Input
515
+ type="number"
516
+ value={priceRange[1]}
517
+ onChange={(e) => setPriceRange([priceRange[0], Number(e.target.value)])}
518
+ prefix="$"
519
+ size="sm"
520
+ />
521
+ </div>
522
+ </div>
523
+ ```
524
+
525
+ ### Zoom Control
526
+
527
+ ```typescript
528
+ const [zoom, setZoom] = useState([100]);
529
+ const zoomLevels = [50, 75, 100, 125, 150, 200];
530
+
531
+ <div className={css({ display: 'flex', alignItems: 'center', gap: 'md' })}>
532
+ <IconButton
533
+ variant="ghost"
534
+ size="sm"
535
+ aria-label="Zoom out"
536
+ onClick={() => setZoom([Math.max(50, zoom[0] - 25)])}
537
+ >
538
+ <MinusIcon />
539
+ </IconButton>
540
+
541
+ <Slider.Root
542
+ className={css({ flex: 1 })}
543
+ min={50}
544
+ max={200}
545
+ step={25}
546
+ value={zoom}
547
+ onValueChange={(details) => setZoom(details.value)}
548
+ >
549
+ <Slider.Control>
550
+ <Slider.Track>
551
+ <Slider.Range />
552
+ </Slider.Track>
553
+ <Slider.Thumb />
554
+ <Slider.MarkerGroup>
555
+ {zoomLevels.map((level) => (
556
+ <Slider.Marker key={level} value={level} />
557
+ ))}
558
+ </Slider.MarkerGroup>
559
+ </Slider.Control>
560
+ </Slider.Root>
561
+
562
+ <IconButton
563
+ variant="ghost"
564
+ size="sm"
565
+ aria-label="Zoom in"
566
+ onClick={() => setZoom([Math.min(200, zoom[0] + 25)])}
567
+ >
568
+ <PlusIcon />
569
+ </IconButton>
570
+
571
+ <span className={css({ minW: '16', textAlign: 'right' })}>{zoom[0]}%</span>
572
+ </div>
573
+ ```
574
+
575
+ ### Settings Panel
576
+
577
+ ```typescript
578
+ const [settings, setSettings] = useState({
579
+ brightness: [75],
580
+ contrast: [50],
581
+ saturation: [60],
582
+ });
583
+
584
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'lg' })}>
585
+ <div>
586
+ <Slider.Root
587
+ value={settings.brightness}
588
+ onValueChange={(details) =>
589
+ setSettings({ ...settings, brightness: details.value })
590
+ }
591
+ >
592
+ <div className={css({ display: 'flex', justifyContent: 'space-between', mb: 'xs' })}>
593
+ <Slider.Label>Brightness</Slider.Label>
594
+ <span>{settings.brightness[0]}%</span>
595
+ </div>
596
+ <Slider.Control>
597
+ <Slider.Track>
598
+ <Slider.Range />
599
+ </Slider.Track>
600
+ <Slider.Thumb />
601
+ </Slider.Control>
602
+ </Slider.Root>
603
+ </div>
604
+
605
+ <div>
606
+ <Slider.Root
607
+ value={settings.contrast}
608
+ onValueChange={(details) =>
609
+ setSettings({ ...settings, contrast: details.value })
610
+ }
611
+ >
612
+ <div className={css({ display: 'flex', justifyContent: 'space-between', mb: 'xs' })}>
613
+ <Slider.Label>Contrast</Slider.Label>
614
+ <span>{settings.contrast[0]}%</span>
615
+ </div>
616
+ <Slider.Control>
617
+ <Slider.Track>
618
+ <Slider.Range />
619
+ </Slider.Track>
620
+ <Slider.Thumb />
621
+ </Slider.Control>
622
+ </Slider.Root>
623
+ </div>
624
+
625
+ <div>
626
+ <Slider.Root
627
+ value={settings.saturation}
628
+ onValueChange={(details) =>
629
+ setSettings({ ...settings, saturation: details.value })
630
+ }
631
+ >
632
+ <div className={css({ display: 'flex', justifyContent: 'space-between', mb: 'xs' })}>
633
+ <Slider.Label>Saturation</Slider.Label>
634
+ <span>{settings.saturation[0]}%</span>
635
+ </div>
636
+ <Slider.Control>
637
+ <Slider.Track>
638
+ <Slider.Range />
639
+ </Slider.Track>
640
+ <Slider.Thumb />
641
+ </Slider.Control>
642
+ </Slider.Root>
643
+ </div>
644
+
645
+ <Button variant="filled" onClick={() => console.log('Save settings:', settings)}>
646
+ Apply Changes
647
+ </Button>
648
+ </div>
649
+ ```
650
+
651
+ ## DO NOT
652
+
653
+ ```typescript
654
+ // ❌ Don't use Slider for precise numeric input
655
+ <Slider.Root min={0} max={999999} defaultValue={[12345]}>
656
+ <Slider.Label>Account Number</Slider.Label>
657
+ {/* ... */}
658
+ </Slider.Root> // Wrong - impossible to select precise value
659
+
660
+ // ✅ Use Input for precise values
661
+ <Input label="Account Number" type="number" />
662
+
663
+ // ❌ Don't omit Track and Range
664
+ <Slider.Root defaultValue={[50]}>
665
+ <Slider.Control>
666
+ <Slider.Thumb /> // Wrong - missing Track/Range
667
+ </Slider.Control>
668
+ </Slider.Root>
669
+
670
+ // ✅ Include Track and Range
671
+ <Slider.Root defaultValue={[50]}>
672
+ <Slider.Control>
673
+ <Slider.Track>
674
+ <Slider.Range />
675
+ </Slider.Track>
676
+ <Slider.Thumb />
677
+ </Slider.Control>
678
+ </Slider.Root>
679
+
680
+ // ❌ Don't use single thumb for range selection
681
+ <Slider.Root min={0} max={1000} defaultValue={[500]}>
682
+ <Slider.Label>Price Range</Slider.Label>
683
+ {/* ... */}
684
+ </Slider.Root> // Wrong - need two thumbs for range
685
+
686
+ // ✅ Use Thumbs component for ranges
687
+ <Slider.Root min={0} max={1000} defaultValue={[100, 500]}>
688
+ <Slider.Label>Price Range</Slider.Label>
689
+ <Slider.Control>
690
+ <Slider.Track>
691
+ <Slider.Range />
692
+ </Slider.Track>
693
+ <Slider.Thumbs>
694
+ {({ thumbs }) => thumbs.map((thumb) => (
695
+ <Slider.Thumb key={thumb} index={thumb} />
696
+ ))}
697
+ </Slider.Thumbs>
698
+ </Slider.Control>
699
+ </Slider.Root>
700
+
701
+ // ❌ Don't override thumb positioning with inline styles
702
+ <Slider.Thumb style={{ left: '50%' }} /> // Wrong - breaks functionality
703
+
704
+ // ❌ Don't use for binary choices
705
+ <Slider.Root min={0} max={1} defaultValue={[1]}>
706
+ <Slider.Label>Enable feature</Slider.Label>
707
+ {/* ... */}
708
+ </Slider.Root> // Wrong - use Switch instead
709
+
710
+ // ✅ Use Switch for on/off
711
+ <Switch.Root>
712
+ <Switch.Label>Enable feature</Switch.Label>
713
+ <Switch.Control><Switch.Thumb /></Switch.Control>
714
+ </Switch.Root>
715
+ ```
716
+
717
+ ## Accessibility
718
+
719
+ The Slider component follows WCAG 2.1 Level AA standards:
720
+
721
+ - **Keyboard Navigation**: Arrow keys to adjust value, Home/End for min/max
722
+ - **Focus Indicator**: Clear focus ring on thumb
723
+ - **ARIA Attributes**: Proper `role="slider"`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`
724
+ - **Labels**: Associated labels for screen readers
725
+ - **Touch Targets**: Minimum 44x44px touch area for thumbs
726
+ - **Value Announcements**: Value changes announced to screen readers
727
+
728
+ ### Accessibility Best Practices
729
+
730
+ ```typescript
731
+ // ✅ Always provide a label
732
+ <Slider.Root defaultValue={[50]}>
733
+ <Slider.Label>Volume</Slider.Label>
734
+ <Slider.Control>
735
+ <Slider.Track>
736
+ <Slider.Range />
737
+ </Slider.Track>
738
+ <Slider.Thumb />
739
+ </Slider.Control>
740
+ </Slider.Root>
741
+
742
+ // ✅ Provide value context
743
+ <Slider.Root min={0} max={100} defaultValue={[50]}>
744
+ <Slider.Label>Volume (%)</Slider.Label>
745
+ <Slider.Control>
746
+ <Slider.Track>
747
+ <Slider.Range />
748
+ </Slider.Track>
749
+ <Slider.Thumb />
750
+ </Slider.Control>
751
+ <Slider.ValueText />
752
+ </Slider.Root>
753
+
754
+ // ✅ Use appropriate step values
755
+ <Slider.Root min={0} max={100} step={5} defaultValue={[50]}>
756
+ <Slider.Label>Volume</Slider.Label>
757
+ <Slider.Control>
758
+ <Slider.Track>
759
+ <Slider.Range />
760
+ </Slider.Track>
761
+ <Slider.Thumb />
762
+ </Slider.Control>
763
+ </Slider.Root>
764
+
765
+ // ✅ Provide visual feedback for ranges
766
+ <Slider.Root min={0} max={1000} defaultValue={[100, 500]}>
767
+ <div className={css({ display: 'flex', justifyContent: 'space-between' })}>
768
+ <Slider.Label>Price Range</Slider.Label>
769
+ <span aria-live="polite">${priceRange[0]} - ${priceRange[1]}</span>
770
+ </div>
771
+ <Slider.Control>
772
+ <Slider.Track>
773
+ <Slider.Range />
774
+ </Slider.Track>
775
+ <Slider.Thumbs>
776
+ {({ thumbs }) => thumbs.map((thumb) => (
777
+ <Slider.Thumb key={thumb} index={thumb} />
778
+ ))}
779
+ </Slider.Thumbs>
780
+ </Slider.Control>
781
+ </Slider.Root>
782
+ ```
783
+
784
+ ## State Behaviors
785
+
786
+ | State | Visual Change | Behavior |
787
+ | ------------ | ------------------------------- | -------------------------- |
788
+ | **Default** | Track with thumb at position | Ready for interaction |
789
+ | **Hover** | Thumb shows hover state | Visual feedback |
790
+ | **Focus** | Focus ring on thumb | Keyboard navigation active |
791
+ | **Dragging** | Thumb follows cursor/touch | Value updates in real-time |
792
+ | **Disabled** | Reduced opacity, no interaction | Cannot be adjusted |
793
+
794
+ ## Responsive Considerations
795
+
796
+ ```typescript
797
+ // Mobile-first: Horizontal slider with larger touch targets
798
+ <Slider.Root size={{ base: 'lg', md: 'md' }} defaultValue={[50]}>
799
+ <Slider.Label>Volume</Slider.Label>
800
+ <Slider.Control>
801
+ <Slider.Track>
802
+ <Slider.Range />
803
+ </Slider.Track>
804
+ <Slider.Thumb />
805
+ </Slider.Control>
806
+ </Slider.Root>
807
+
808
+ // Responsive width
809
+ <div className={css({ width: { base: 'full', md: '400px' } })}>
810
+ <Slider.Root defaultValue={[50]}>
811
+ <Slider.Label>Brightness</Slider.Label>
812
+ <Slider.Control>
813
+ <Slider.Track>
814
+ <Slider.Range />
815
+ </Slider.Track>
816
+ <Slider.Thumb />
817
+ </Slider.Control>
818
+ </Slider.Root>
819
+ </div>
820
+
821
+ // Vertical on desktop, horizontal on mobile
822
+ <Slider.Root
823
+ orientation={{ base: 'horizontal', md: 'vertical' }}
824
+ defaultValue={[50]}
825
+ className={css({ height: { md: '200px' } })}
826
+ >
827
+ <Slider.Label>Volume</Slider.Label>
828
+ <Slider.Control>
829
+ <Slider.Track>
830
+ <Slider.Range />
831
+ </Slider.Track>
832
+ <Slider.Thumb />
833
+ </Slider.Control>
834
+ </Slider.Root>
835
+ ```
836
+
837
+ ## Testing
838
+
839
+ ```typescript
840
+ import { render, screen } from '@testing-library/react';
841
+ import userEvent from '@testing-library/user-event';
842
+
843
+ test('slider updates value on interaction', async () => {
844
+ const handleChange = vi.fn();
845
+
846
+ render(
847
+ <Slider.Root defaultValue={[50]} onValueChange={handleChange}>
848
+ <Slider.Label>Volume</Slider.Label>
849
+ <Slider.Control>
850
+ <Slider.Track>
851
+ <Slider.Range />
852
+ </Slider.Track>
853
+ <Slider.Thumb />
854
+ </Slider.Control>
855
+ </Slider.Root>
856
+ );
857
+
858
+ const slider = screen.getByRole('slider', { name: 'Volume' });
859
+
860
+ // Simulate keyboard interaction
861
+ slider.focus();
862
+ await userEvent.keyboard('{ArrowRight}');
863
+
864
+ expect(handleChange).toHaveBeenCalled();
865
+ });
866
+
867
+ test('disabled slider cannot be adjusted', () => {
868
+ render(
869
+ <Slider.Root defaultValue={[50]} disabled>
870
+ <Slider.Label>Volume</Slider.Label>
871
+ <Slider.Control>
872
+ <Slider.Track>
873
+ <Slider.Range />
874
+ </Slider.Track>
875
+ <Slider.Thumb />
876
+ </Slider.Control>
877
+ </Slider.Root>
878
+ );
879
+
880
+ const slider = screen.getByRole('slider');
881
+ expect(slider).toHaveAttribute('aria-disabled', 'true');
882
+ });
883
+
884
+ test('range slider has multiple thumbs', () => {
885
+ render(
886
+ <Slider.Root defaultValue={[25, 75]}>
887
+ <Slider.Label>Range</Slider.Label>
888
+ <Slider.Control>
889
+ <Slider.Track>
890
+ <Slider.Range />
891
+ </Slider.Track>
892
+ <Slider.Thumbs>
893
+ {({ thumbs }) => thumbs.map((thumb) => (
894
+ <Slider.Thumb key={thumb} index={thumb} />
895
+ ))}
896
+ </Slider.Thumbs>
897
+ </Slider.Control>
898
+ </Slider.Root>
899
+ );
900
+
901
+ const sliders = screen.getAllByRole('slider');
902
+ expect(sliders).toHaveLength(2);
903
+ });
904
+ ```
905
+
906
+ ## Related Components
907
+
908
+ - **Input** - For precise numeric entry
909
+ - **Switch** - For binary on/off states
910
+ - **RadioGroup** - For discrete option selection
911
+ - **Select** - For choosing from predefined options