@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,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
|