@ui-sdk/react 0.0.0-stage → 0.1.0-beta.1

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 (247) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +331 -0
  4. package/dist/accordion.js +25 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +8 -0
  7. package/dist/alert-dialog.d.ts +56 -0
  8. package/dist/alert-dialog.js +45 -0
  9. package/dist/async-options.d.ts +22 -0
  10. package/dist/async-options.js +56 -0
  11. package/dist/autocomplete.d.ts +102 -0
  12. package/dist/autocomplete.js +195 -0
  13. package/dist/avatar.d.ts +301 -0
  14. package/dist/avatar.js +17 -0
  15. package/dist/button.d.ts +21 -0
  16. package/dist/button.js +45 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +9 -0
  19. package/dist/calendar.d.ts +80 -0
  20. package/dist/calendar.js +257 -0
  21. package/dist/carousel.d.ts +52 -0
  22. package/dist/carousel.js +151 -0
  23. package/dist/checkbox.d.ts +24 -0
  24. package/dist/checkbox.js +98 -0
  25. package/dist/children.d.ts +38 -0
  26. package/dist/children.js +63 -0
  27. package/dist/collapsible.d.ts +302 -0
  28. package/dist/collapsible.js +19 -0
  29. package/dist/color-picker.d.ts +72 -0
  30. package/dist/color-picker.js +133 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +22 -0
  33. package/dist/combobox.d.ts +152 -0
  34. package/dist/combobox.js +338 -0
  35. package/dist/command.d.ts +88 -0
  36. package/dist/command.js +233 -0
  37. package/dist/controller-target.d.ts +3 -0
  38. package/dist/controller-target.js +59 -0
  39. package/dist/date-input.d.ts +19 -0
  40. package/dist/date-input.js +58 -0
  41. package/dist/date-picker.d.ts +47 -0
  42. package/dist/date-picker.js +310 -0
  43. package/dist/date.d.ts +2 -0
  44. package/dist/date.js +60 -0
  45. package/dist/dialog-parts.d.ts +23 -0
  46. package/dist/dialog-parts.js +142 -0
  47. package/dist/dialog.d.ts +57 -0
  48. package/dist/dialog.js +32 -0
  49. package/dist/drawer.d.ts +70 -0
  50. package/dist/drawer.js +158 -0
  51. package/dist/editor-context.d.ts +4 -0
  52. package/dist/editor-context.js +9 -0
  53. package/dist/field-context.d.ts +47 -0
  54. package/dist/field-context.js +87 -0
  55. package/dist/field.d.ts +49 -0
  56. package/dist/field.js +169 -0
  57. package/dist/file-input.d.ts +46 -0
  58. package/dist/file-input.js +170 -0
  59. package/dist/form-inputs.d.ts +5 -0
  60. package/dist/form-inputs.js +22 -0
  61. package/dist/gesture.d.ts +1 -0
  62. package/dist/gesture.js +16 -0
  63. package/dist/history.d.ts +2 -0
  64. package/dist/history.js +12 -0
  65. package/dist/hover-card.d.ts +33 -0
  66. package/dist/hover-card.js +84 -0
  67. package/dist/image-cropper.d.ts +333 -0
  68. package/dist/image-cropper.js +37 -0
  69. package/dist/image.d.ts +2 -0
  70. package/dist/image.js +6 -0
  71. package/dist/input.d.ts +22 -0
  72. package/dist/input.js +51 -0
  73. package/dist/label.d.ts +294 -0
  74. package/dist/label.js +9 -0
  75. package/dist/list-items.d.ts +15 -0
  76. package/dist/list-items.js +39 -0
  77. package/dist/list-text.d.ts +4 -0
  78. package/dist/list-text.js +15 -0
  79. package/dist/listbox.d.ts +56 -0
  80. package/dist/listbox.js +122 -0
  81. package/dist/menu.d.ts +116 -0
  82. package/dist/menu.js +426 -0
  83. package/dist/menubar-context.d.ts +4 -0
  84. package/dist/menubar-context.js +7 -0
  85. package/dist/menubar.d.ts +11 -0
  86. package/dist/menubar.js +32 -0
  87. package/dist/meter.d.ts +312 -0
  88. package/dist/meter.js +30 -0
  89. package/dist/navigation-menu.d.ts +61 -0
  90. package/dist/navigation-menu.js +169 -0
  91. package/dist/number-input.d.ts +44 -0
  92. package/dist/number-input.js +185 -0
  93. package/dist/number.d.ts +2 -0
  94. package/dist/number.js +40 -0
  95. package/dist/observe.d.ts +2 -0
  96. package/dist/observe.js +8 -0
  97. package/dist/otp-input.d.ts +29 -0
  98. package/dist/otp-input.js +94 -0
  99. package/dist/overlays.d.ts +26 -0
  100. package/dist/overlays.js +82 -0
  101. package/dist/pagination.d.ts +336 -0
  102. package/dist/pagination.js +71 -0
  103. package/dist/password-input.d.ts +23 -0
  104. package/dist/password-input.js +65 -0
  105. package/dist/popover.d.ts +54 -0
  106. package/dist/popover.js +151 -0
  107. package/dist/popup.d.ts +41 -0
  108. package/dist/popup.js +180 -0
  109. package/dist/preferences.d.ts +35 -0
  110. package/dist/preferences.js +93 -0
  111. package/dist/primitive.d.ts +2 -0
  112. package/dist/primitive.js +14 -0
  113. package/dist/progress.d.ts +312 -0
  114. package/dist/progress.js +30 -0
  115. package/dist/radio.d.ts +22 -0
  116. package/dist/radio.js +86 -0
  117. package/dist/rating.d.ts +20 -0
  118. package/dist/rating.js +94 -0
  119. package/dist/render-element.d.ts +35 -0
  120. package/dist/render-element.js +103 -0
  121. package/dist/resizable.d.ts +302 -0
  122. package/dist/resizable.js +23 -0
  123. package/dist/scroll-area.d.ts +324 -0
  124. package/dist/scroll-area.js +49 -0
  125. package/dist/segmented-control.d.ts +24 -0
  126. package/dist/segmented-control.js +78 -0
  127. package/dist/segmented-input.d.ts +15 -0
  128. package/dist/segmented-input.js +48 -0
  129. package/dist/select.d.ts +125 -0
  130. package/dist/select.js +330 -0
  131. package/dist/separator.d.ts +290 -0
  132. package/dist/separator.js +9 -0
  133. package/dist/shortcuts.d.ts +32 -0
  134. package/dist/shortcuts.js +61 -0
  135. package/dist/slider.d.ts +38 -0
  136. package/dist/slider.js +163 -0
  137. package/dist/switch.d.ts +15 -0
  138. package/dist/switch.js +39 -0
  139. package/dist/tabs.d.ts +331 -0
  140. package/dist/tabs.js +28 -0
  141. package/dist/text-control.d.ts +22 -0
  142. package/dist/text-control.js +24 -0
  143. package/dist/text-match.d.ts +2 -0
  144. package/dist/text-match.js +8 -0
  145. package/dist/textarea.d.ts +10 -0
  146. package/dist/textarea.js +48 -0
  147. package/dist/time-input.d.ts +19 -0
  148. package/dist/time-input.js +63 -0
  149. package/dist/timeline.d.ts +327 -0
  150. package/dist/timeline.js +31 -0
  151. package/dist/toast-parts.d.ts +63 -0
  152. package/dist/toast-parts.js +144 -0
  153. package/dist/toast.d.ts +2 -0
  154. package/dist/toast.js +22 -0
  155. package/dist/toggle.d.ts +23 -0
  156. package/dist/toggle.js +76 -0
  157. package/dist/token-input.d.ts +35 -0
  158. package/dist/token-input.js +100 -0
  159. package/dist/toolbar-context.d.ts +6 -0
  160. package/dist/toolbar-context.js +16 -0
  161. package/dist/toolbar.d.ts +38 -0
  162. package/dist/toolbar.js +63 -0
  163. package/dist/tooltip.d.ts +39 -0
  164. package/dist/tooltip.js +108 -0
  165. package/dist/tree.d.ts +51 -0
  166. package/dist/tree.js +150 -0
  167. package/dist/trigger-menu.d.ts +83 -0
  168. package/dist/trigger-menu.js +167 -0
  169. package/dist/use-primitive.d.ts +178 -0
  170. package/dist/use-primitive.js +674 -0
  171. package/dist/waterfall.d.ts +80 -0
  172. package/dist/waterfall.js +108 -0
  173. package/docs/README.md +89 -0
  174. package/docs/agents.md +1523 -0
  175. package/docs/components/accordion.md +1391 -0
  176. package/docs/components/alert-dialog.md +850 -0
  177. package/docs/components/autocomplete.md +1963 -0
  178. package/docs/components/avatar.md +252 -0
  179. package/docs/components/button.md +444 -0
  180. package/docs/components/calendar.md +1687 -0
  181. package/docs/components/carousel.md +1244 -0
  182. package/docs/components/checkbox.md +1069 -0
  183. package/docs/components/collapsible.md +567 -0
  184. package/docs/components/color-picker.md +736 -0
  185. package/docs/components/combobox.md +3647 -0
  186. package/docs/components/command.md +1573 -0
  187. package/docs/components/date-input.md +608 -0
  188. package/docs/components/date-picker.md +1266 -0
  189. package/docs/components/dialog.md +1469 -0
  190. package/docs/components/drawer.md +1197 -0
  191. package/docs/components/field.md +549 -0
  192. package/docs/components/file-input.md +609 -0
  193. package/docs/components/hover-card.md +514 -0
  194. package/docs/components/image-cropper.md +579 -0
  195. package/docs/components/input.md +301 -0
  196. package/docs/components/label.md +217 -0
  197. package/docs/components/listbox.md +1130 -0
  198. package/docs/components/menu.md +1286 -0
  199. package/docs/components/menubar.md +455 -0
  200. package/docs/components/meter.md +304 -0
  201. package/docs/components/navigation-menu.md +1581 -0
  202. package/docs/components/number-input.md +1086 -0
  203. package/docs/components/otp-input.md +565 -0
  204. package/docs/components/pagination.md +648 -0
  205. package/docs/components/password-input.md +737 -0
  206. package/docs/components/popover.md +1552 -0
  207. package/docs/components/preferences.md +220 -0
  208. package/docs/components/progress.md +443 -0
  209. package/docs/components/radio.md +846 -0
  210. package/docs/components/rating.md +458 -0
  211. package/docs/components/resizable.md +982 -0
  212. package/docs/components/scroll-area.md +980 -0
  213. package/docs/components/segmented-control.md +465 -0
  214. package/docs/components/select.md +2133 -0
  215. package/docs/components/separator.md +243 -0
  216. package/docs/components/slider.md +1169 -0
  217. package/docs/components/switch.md +613 -0
  218. package/docs/components/tabs.md +1014 -0
  219. package/docs/components/textarea.md +318 -0
  220. package/docs/components/time-input.md +231 -0
  221. package/docs/components/timeline.md +677 -0
  222. package/docs/components/toast.md +1395 -0
  223. package/docs/components/toggle.md +541 -0
  224. package/docs/components/token-input.md +1007 -0
  225. package/docs/components/toolbar.md +787 -0
  226. package/docs/components/tooltip.md +884 -0
  227. package/docs/components/tree.md +1346 -0
  228. package/docs/components/trigger-menu.md +1257 -0
  229. package/docs/components/waterfall.md +754 -0
  230. package/docs/controllers.md +89 -0
  231. package/docs/core.md +8027 -0
  232. package/docs/custom-parts.md +120 -0
  233. package/docs/first-component.md +83 -0
  234. package/docs/forms.md +148 -0
  235. package/docs/hooks/async-options.md +366 -0
  236. package/docs/hooks/shortcuts.md +221 -0
  237. package/docs/installation.md +45 -0
  238. package/docs/introduction.md +32 -0
  239. package/docs/lists-and-menus.md +170 -0
  240. package/docs/llms.txt +1844 -0
  241. package/docs/parts-and-composition.md +93 -0
  242. package/docs/popups.md +117 -0
  243. package/docs/rendering.md +121 -0
  244. package/docs/server-rendering.md +253 -0
  245. package/docs/state-and-events.md +105 -0
  246. package/docs/styling-and-animation.md +176 -0
  247. package/package.json +333 -3
@@ -0,0 +1,1169 @@
1
+ # Slider
2
+
3
+ Adjust a number or a range with draggable thumbs and keyboard controls.
4
+
5
+ Use a slider to pick a number or a range along a scale, like a volume or a price range, by dragging a thumb or with the arrow keys.
6
+
7
+ <ComponentExample
8
+ name="slider/basic"
9
+ inspect
10
+ caption="Drag the thumb, or focus it and use the arrow keys, to adjust Volume."
11
+ />
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @ui-sdk/react @ui-sdk/core
17
+ ```
18
+
19
+ ## Composition
20
+
21
+ Use the following composition to build a `Slider`:
22
+
23
+ ```text
24
+ Slider
25
+ ├── SliderValue
26
+ └── SliderControl
27
+ └── SliderTrack
28
+ ├── SliderIndicator
29
+ └── SliderThumb
30
+ ```
31
+
32
+ A control handles presses around the track. Put a thumb on that track for each value.
33
+
34
+ - **Slider** holds the value and is a group around the thumbs.
35
+ - **SliderControl** is where presses land. Pressing it moves the nearest thumb and starts a drag. Give it some height around a thin track.
36
+ - **SliderTrack** is the rail the thumbs move along. Thumbs and the indicator are placed in percentages of it with inline styles, so give it a size. It's the positioned box.
37
+ - **SliderIndicator** fills the track up to a single thumb, or between the first and last thumb of a range.
38
+ - **SliderThumb** is what you drag. Give each one a size.
39
+ - **SliderValue** shows the formatted value, and a range joined with an en dash. It's optional.
40
+
41
+ Each thumb renders a visually hidden `<input type="range">` inside itself. That input is what takes focus, so
42
+ screen readers on phones adjust it with their own gestures. Style focus on the thumb with `:has(:focus-visible)`.
43
+ The slider also renders a hidden input that the form submits.
44
+
45
+ A single thumb is named by a `FieldLabel` in a `Field`, or by `aria-label` on the thumb. In a range, give
46
+ each thumb its own `aria-label`, like "Minimum price", and name the group with a `FieldLabel` or
47
+ `aria-label` on the slider.
48
+
49
+ ## Usage
50
+
51
+ Import the parts and put them together:
52
+
53
+ ```tsx
54
+ "use client"
55
+
56
+ import {
57
+ Slider,
58
+ SliderValue,
59
+ SliderControl,
60
+ SliderTrack,
61
+ SliderIndicator,
62
+ SliderThumb,
63
+ } from "@ui-sdk/react/slider"
64
+
65
+ function VolumeSlider() {
66
+ return (
67
+ <Slider defaultValue={50}>
68
+ <SliderValue />
69
+ <SliderControl>
70
+ <SliderTrack>
71
+ <SliderIndicator />
72
+ <SliderThumb aria-label="Volume" />
73
+ </SliderTrack>
74
+ </SliderControl>
75
+ </Slider>
76
+ )
77
+ }
78
+
79
+ export { VolumeSlider }
80
+ ```
81
+
82
+ ## Range
83
+
84
+ Pass an array for a range, then render one thumb per number. Give each thumb its `index`, from 0, and its own name, like "Minimum price". The indicator fills the space between the first and the last thumb, and the `FieldLabel` names the group.
85
+
86
+ ```tsx title="range.tsx"
87
+ "use client"
88
+
89
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
90
+ import {
91
+ Slider,
92
+ SliderControl,
93
+ SliderIndicator,
94
+ SliderThumb,
95
+ SliderTrack,
96
+ SliderValue,
97
+ } from "@ui-sdk/react/slider"
98
+
99
+ function Example() {
100
+ return (
101
+ <Field>
102
+ <Slider
103
+ defaultValue={[25, 50]}
104
+ step={5}
105
+ format={{
106
+ style: "currency",
107
+ currency: "USD",
108
+ maximumFractionDigits: 0,
109
+ }}
110
+ >
111
+ <div data-slot="row">
112
+ <FieldLabel>Price</FieldLabel>
113
+ <SliderValue />
114
+ </div>
115
+ <SliderControl>
116
+ <SliderTrack>
117
+ <SliderIndicator />
118
+ <SliderThumb index={0} aria-label="Minimum price" />
119
+ <SliderThumb index={1} aria-label="Maximum price" />
120
+ </SliderTrack>
121
+ </SliderControl>
122
+ </Slider>
123
+ </Field>
124
+ )
125
+ }
126
+
127
+ export { Example }
128
+ ```
129
+
130
+ Move either end of Price. The thumbs stay in order.
131
+
132
+ ## Multiple Thumbs
133
+
134
+ Any number of thumbs works the same way. They never pass each other, and a press on the track moves the nearest one.
135
+
136
+ ```tsx title="multiple.tsx"
137
+ "use client"
138
+
139
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
140
+ import {
141
+ Slider,
142
+ SliderControl,
143
+ SliderIndicator,
144
+ SliderThumb,
145
+ SliderTrack,
146
+ SliderValue,
147
+ } from "@ui-sdk/react/slider"
148
+
149
+ const thumbs = ["Low", "Medium", "High"]
150
+
151
+ function Example() {
152
+ return (
153
+ <Field>
154
+ <Slider defaultValue={[10, 20, 70]} step={10} format={{ style: "unit", unit: "percent" }}>
155
+ <div data-slot="row">
156
+ <FieldLabel>Alert thresholds</FieldLabel>
157
+ <SliderValue />
158
+ </div>
159
+ <SliderControl>
160
+ <SliderTrack>
161
+ <SliderIndicator />
162
+ {thumbs.map((label, index) => (
163
+ <SliderThumb key={label} index={index} aria-label={label} />
164
+ ))}
165
+ </SliderTrack>
166
+ </SliderControl>
167
+ </Slider>
168
+ </Field>
169
+ )
170
+ }
171
+
172
+ export { Example }
173
+ ```
174
+
175
+ Adjust Low, Medium and High independently, without moving one past another.
176
+
177
+ ## Vertical
178
+
179
+ Set `orientation="vertical"` and give the slider a height. The bottom is the minimum, ArrowUp increases the value, and every part gets `data-orientation`, so `[data-orientation="vertical"]` styles the vertical axis.
180
+
181
+ ```tsx title="vertical.tsx"
182
+ "use client"
183
+
184
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
185
+ import {
186
+ Slider,
187
+ SliderControl,
188
+ SliderIndicator,
189
+ SliderThumb,
190
+ SliderTrack,
191
+ } from "@ui-sdk/react/slider"
192
+
193
+ const channels = [
194
+ { label: "Bass", value: 50 },
195
+ { label: "Treble", value: 25 },
196
+ ]
197
+
198
+ function Example() {
199
+ return (
200
+ <div data-slot="row">
201
+ {channels.map((channel) => (
202
+ <Field key={channel.label}>
203
+ <Slider orientation="vertical" defaultValue={channel.value}>
204
+ <SliderControl>
205
+ <SliderTrack>
206
+ <SliderIndicator />
207
+ <SliderThumb />
208
+ </SliderTrack>
209
+ </SliderControl>
210
+ </Slider>
211
+ <FieldLabel>{channel.label}</FieldLabel>
212
+ </Field>
213
+ ))}
214
+ </div>
215
+ )
216
+ }
217
+
218
+ export { Example }
219
+ ```
220
+
221
+ Drag up or down, or use the arrow keys, to adjust Bass and Treble.
222
+
223
+ ## Field
224
+
225
+ Put the slider in a `Field`. The label names the thumb, the description describes it, and `SliderValue` shows the value beside the label. Screen readers don't announce `SliderValue` as it changes, since the thumb already says its value.
226
+
227
+ ```tsx title="field.tsx"
228
+ "use client"
229
+
230
+ import { Field, FieldDescription, FieldLabel } from "@ui-sdk/react/field"
231
+ import {
232
+ Slider,
233
+ SliderControl,
234
+ SliderIndicator,
235
+ SliderThumb,
236
+ SliderTrack,
237
+ SliderValue,
238
+ } from "@ui-sdk/react/slider"
239
+
240
+ function Example() {
241
+ return (
242
+ <Field>
243
+ <Slider defaultValue={0.3} format={{ style: "percent" }} min={0} max={1} step={0.01}>
244
+ <div data-slot="row">
245
+ <FieldLabel>Volume</FieldLabel>
246
+ <SliderValue />
247
+ </div>
248
+ <SliderControl>
249
+ <SliderTrack>
250
+ <SliderIndicator />
251
+ <SliderThumb />
252
+ </SliderTrack>
253
+ </SliderControl>
254
+ </Slider>
255
+ <FieldDescription>How loud notifications play.</FieldDescription>
256
+ </Field>
257
+ )
258
+ }
259
+
260
+ export { Example }
261
+ ```
262
+
263
+ Click the Volume label to focus the thumb, then use the arrow keys to change the value.
264
+
265
+ ## Disabled
266
+
267
+ Set `disabled`. A disabled slider can't be focused or moved, and isn't submitted. A disabled `<fieldset>` around it disables it too.
268
+
269
+ ```tsx title="disabled.tsx"
270
+ "use client"
271
+
272
+ import { Field, FieldDescription, FieldLabel } from "@ui-sdk/react/field"
273
+ import {
274
+ Slider,
275
+ SliderControl,
276
+ SliderIndicator,
277
+ SliderThumb,
278
+ SliderTrack,
279
+ SliderValue,
280
+ } from "@ui-sdk/react/slider"
281
+
282
+ function Example() {
283
+ return (
284
+ <Field>
285
+ <Slider defaultValue={50} disabled>
286
+ <div data-slot="row">
287
+ <FieldLabel>Brightness</FieldLabel>
288
+ <SliderValue />
289
+ </div>
290
+ <SliderControl>
291
+ <SliderTrack>
292
+ <SliderIndicator />
293
+ <SliderThumb />
294
+ </SliderTrack>
295
+ </SliderControl>
296
+ </Slider>
297
+ <FieldDescription>Managed by your organization.</FieldDescription>
298
+ </Field>
299
+ )
300
+ }
301
+
302
+ export { Example }
303
+ ```
304
+
305
+ Brightness keeps its value, but can't be focused or dragged.
306
+
307
+ ## Controlled
308
+
309
+ Pass `value` and `onValueChange` to control the slider. The value keeps its shape: a number stays a number and an array stays an array. Every change carries a [reason](#reasons), like `drag`, `track-press` or `step-key`, and `details.thumb` says which thumb moved. Call `details.cancel()` to keep the current value.
310
+
311
+ ```tsx title="controlled.tsx"
312
+ "use client"
313
+
314
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
315
+ import {
316
+ Slider,
317
+ SliderControl,
318
+ SliderIndicator,
319
+ SliderThumb,
320
+ SliderTrack,
321
+ SliderValue,
322
+ } from "@ui-sdk/react/slider"
323
+ import * as React from "react"
324
+ import { useLog } from "./log"
325
+
326
+ function Example() {
327
+ const log = useLog()
328
+ const [value, setValue] = React.useState([0.3, 0.7])
329
+
330
+ return (
331
+ <div data-slot="stack">
332
+ <div data-slot="controls">
333
+ <button type="button" onClick={() => setValue([0.3, 0.7])}>
334
+ Reset
335
+ </button>
336
+ </div>
337
+ <Field>
338
+ <Slider
339
+ value={value}
340
+ onValueChange={(next, details) => {
341
+ setValue(next)
342
+ log(`${next.join(" – ")} (${details.reason})`)
343
+ }}
344
+ min={0}
345
+ max={1}
346
+ step={0.1}
347
+ >
348
+ <div data-slot="row">
349
+ <FieldLabel>Temperature</FieldLabel>
350
+ <SliderValue />
351
+ </div>
352
+ <SliderControl>
353
+ <SliderTrack>
354
+ <SliderIndicator />
355
+ <SliderThumb index={0} aria-label="Minimum temperature" />
356
+ <SliderThumb index={1} aria-label="Maximum temperature" />
357
+ </SliderTrack>
358
+ </SliderControl>
359
+ </Slider>
360
+ </Field>
361
+ </div>
362
+ )
363
+ }
364
+
365
+ export { Example }
366
+ ```
367
+
368
+ Drag either Temperature thumb to log the range and its reason. Reset sets the value from outside.
369
+
370
+ ## Step and Bounds
371
+
372
+ `min` and `max` set the bounds, 0 and 100 by default, and `step` sets how far one step moves, counted from `min`. Decimal steps avoid floating-point drift, so a 0.1 step can reach 0.3 cleanly. When the steps from `min` don't land on `max`, the highest step below it is the end.
373
+
374
+ ```tsx title="step.tsx"
375
+ "use client"
376
+
377
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
378
+ import {
379
+ Slider,
380
+ SliderControl,
381
+ SliderIndicator,
382
+ SliderThumb,
383
+ SliderTrack,
384
+ SliderValue,
385
+ } from "@ui-sdk/react/slider"
386
+
387
+ function Ticks({ count }: { count: number }) {
388
+ return (
389
+ <div data-slot="ticks" aria-hidden="true">
390
+ {Array.from({ length: count }, (_, index) => (
391
+ <span key={index} />
392
+ ))}
393
+ </div>
394
+ )
395
+ }
396
+
397
+ function Example() {
398
+ return (
399
+ <div data-slot="stack">
400
+ <Field>
401
+ <Slider defaultValue={2010} min={1990} max={2030} step={5} format={{ useGrouping: false }}>
402
+ <div data-slot="row">
403
+ <FieldLabel>Year</FieldLabel>
404
+ <SliderValue />
405
+ </div>
406
+ <SliderControl>
407
+ <SliderTrack>
408
+ <SliderIndicator />
409
+ <SliderThumb />
410
+ </SliderTrack>
411
+ </SliderControl>
412
+ <Ticks count={9} />
413
+ </Slider>
414
+ </Field>
415
+ <Field>
416
+ <Slider defaultValue={0.5} min={0} max={1} step={0.05}>
417
+ <div data-slot="row">
418
+ <FieldLabel>Opacity</FieldLabel>
419
+ <SliderValue />
420
+ </div>
421
+ <SliderControl>
422
+ <SliderTrack>
423
+ <SliderIndicator />
424
+ <SliderThumb />
425
+ </SliderTrack>
426
+ </SliderControl>
427
+ <Ticks count={21} />
428
+ </Slider>
429
+ </Field>
430
+ </div>
431
+ )
432
+ }
433
+
434
+ export { Example }
435
+ ```
436
+
437
+ Move Year in steps of five, and Opacity in steps of 0.05.
438
+
439
+ ## Large Step
440
+
441
+ Arrow keys move one step. Page Up, Page Down and Shift with an arrow key move by `largeStep`, which is ten steps unless you set it. Home and End go to the bounds.
442
+
443
+ ```tsx title="large-step.tsx"
444
+ "use client"
445
+
446
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
447
+ import {
448
+ Slider,
449
+ SliderControl,
450
+ SliderIndicator,
451
+ SliderThumb,
452
+ SliderTrack,
453
+ SliderValue,
454
+ } from "@ui-sdk/react/slider"
455
+
456
+ function Example() {
457
+ return (
458
+ <Field>
459
+ <Slider
460
+ defaultValue={100}
461
+ min={25}
462
+ max={400}
463
+ largeStep={25}
464
+ format={{ style: "unit", unit: "percent" }}
465
+ >
466
+ <div data-slot="row">
467
+ <FieldLabel>Zoom</FieldLabel>
468
+ <SliderValue />
469
+ </div>
470
+ <SliderControl>
471
+ <SliderTrack>
472
+ <SliderIndicator />
473
+ <SliderThumb />
474
+ </SliderTrack>
475
+ </SliderControl>
476
+ </Slider>
477
+ </Field>
478
+ )
479
+ }
480
+
481
+ export { Example }
482
+ ```
483
+
484
+ Focus Zoom and press an arrow key to move 1%, then Page Up, Page Down or Shift with an arrow key to move 25%.
485
+
486
+ ## Distance Between Thumbs
487
+
488
+ Set `minStepsBetweenThumbs` to keep neighbouring thumbs that many steps apart. A thumb stops at its neighbour, and Home and End move it only as far as its neighbour allows.
489
+
490
+ ```tsx title="min-steps.tsx"
491
+ "use client"
492
+
493
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
494
+ import {
495
+ Slider,
496
+ SliderControl,
497
+ SliderIndicator,
498
+ SliderThumb,
499
+ SliderTrack,
500
+ SliderValue,
501
+ } from "@ui-sdk/react/slider"
502
+
503
+ const time = (minutes: number) =>
504
+ `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}`
505
+
506
+ function Example() {
507
+ return (
508
+ <Field>
509
+ <Slider
510
+ defaultValue={[540, 1020]}
511
+ min={0}
512
+ max={1440}
513
+ step={15}
514
+ minStepsBetweenThumbs={16}
515
+ format={time}
516
+ >
517
+ <div data-slot="row">
518
+ <FieldLabel>Opening hours</FieldLabel>
519
+ <SliderValue />
520
+ </div>
521
+ <SliderControl>
522
+ <SliderTrack>
523
+ <SliderIndicator />
524
+ <SliderThumb index={0} aria-label="Opens" />
525
+ <SliderThumb index={1} aria-label="Closes" />
526
+ </SliderTrack>
527
+ </SliderControl>
528
+ </Slider>
529
+ </Field>
530
+ )
531
+ }
532
+
533
+ export { Example }
534
+ ```
535
+
536
+ Move either end of Opening hours. The thumbs stay at least four hours apart.
537
+
538
+ ## Format and Locale
539
+
540
+ `format` takes `Intl.NumberFormat` options, like `{ style: "currency", currency: "USD" }` or `{ style: "percent" }`, or a function of the value. It sets what `SliderValue` shows and what screen readers announce for each thumb, while the value stays a plain number. `locale` sets separators, digits and symbol placement. Without it, the slider uses the page's `lang`, or the browser's language, once the page is interactive. Server markup uses English until then, so it never disagrees with the client.
541
+
542
+ ```tsx title="format.tsx"
543
+ "use client"
544
+
545
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
546
+ import {
547
+ Slider,
548
+ SliderControl,
549
+ SliderIndicator,
550
+ SliderThumb,
551
+ SliderTrack,
552
+ SliderValue,
553
+ } from "@ui-sdk/react/slider"
554
+
555
+ const sliders = [
556
+ {
557
+ label: "Budget",
558
+ defaultValue: 40,
559
+ max: 100,
560
+ step: 1,
561
+ format: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
562
+ },
563
+ {
564
+ label: "Opacity",
565
+ defaultValue: 0.6,
566
+ max: 1,
567
+ step: 0.01,
568
+ format: { style: "percent" },
569
+ },
570
+ {
571
+ label: "Weight",
572
+ defaultValue: 20,
573
+ max: 50,
574
+ step: 0.5,
575
+ format: { style: "unit", unit: "kilogram" },
576
+ },
577
+ {
578
+ label: "Duration",
579
+ defaultValue: 90,
580
+ max: 600,
581
+ step: 15,
582
+ format: (seconds: number) =>
583
+ `${Math.floor(seconds / 60)} min ${seconds % 60} s`,
584
+ },
585
+ {
586
+ label: "Preis",
587
+ defaultValue: 40,
588
+ max: 100,
589
+ step: 1,
590
+ format: { style: "currency", currency: "EUR" },
591
+ locale: "de-DE",
592
+ },
593
+ ] satisfies {
594
+ label: string
595
+ defaultValue: number
596
+ max: number
597
+ step: number
598
+ format: Intl.NumberFormatOptions | ((value: number) => string)
599
+ locale?: string
600
+ }[]
601
+
602
+ function Example() {
603
+ return (
604
+ <div data-slot="stack">
605
+ {sliders.map(({ label, ...options }) => (
606
+ <Field key={label}>
607
+ <Slider {...options}>
608
+ <div data-slot="row">
609
+ <FieldLabel>{label}</FieldLabel>
610
+ <SliderValue />
611
+ </div>
612
+ <SliderControl>
613
+ <SliderTrack>
614
+ <SliderIndicator />
615
+ <SliderThumb />
616
+ </SliderTrack>
617
+ </SliderControl>
618
+ </Slider>
619
+ </Field>
620
+ ))}
621
+ </div>
622
+ )
623
+ }
624
+
625
+ export { Example }
626
+ ```
627
+
628
+ Adjust Budget, Opacity, Weight, Duration and Preis to compare currency, a percentage, a unit, a custom function and German formatting.
629
+
630
+ ## Saving on Commit
631
+
632
+ `onValueChange` runs on every step of a drag. `onValueCommit` runs once, when the interaction that changed the value ends: the pointer lifts, the key comes up, or a screen reader adjusts it. Use the commit callback to save once per interaction. A press that doesn't change the value commits nothing.
633
+
634
+ ```tsx title="commit.tsx"
635
+ "use client"
636
+
637
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
638
+ import {
639
+ Slider,
640
+ SliderControl,
641
+ SliderIndicator,
642
+ SliderThumb,
643
+ SliderTrack,
644
+ SliderValue,
645
+ } from "@ui-sdk/react/slider"
646
+ import { useLog } from "./log"
647
+
648
+ function Example() {
649
+ const log = useLog()
650
+
651
+ return (
652
+ <Field>
653
+ <Slider
654
+ defaultValue={50}
655
+ onValueCommit={(value, details) =>
656
+ log(`Saved ${value} (${details.reason})`)
657
+ }
658
+ >
659
+ <div data-slot="row">
660
+ <FieldLabel>Brightness</FieldLabel>
661
+ <SliderValue />
662
+ </div>
663
+ <SliderControl>
664
+ <SliderTrack>
665
+ <SliderIndicator />
666
+ <SliderThumb />
667
+ </SliderTrack>
668
+ </SliderControl>
669
+ </Slider>
670
+ </Field>
671
+ )
672
+ }
673
+
674
+ export { Example }
675
+ ```
676
+
677
+ Drag Brightness and release it, or press an arrow key. The console logs the value once, when you let go.
678
+
679
+ ## Controller
680
+
681
+ A controller, from `createSliderController()`, moves the slider from anywhere with `setValue()`, reads it with `getValue()` and focuses a thumb with `focus()`. Values set through it are clamped, snapped to the step and sorted, and reported with the `imperative` reason.
682
+
683
+ ```tsx title="controller.tsx"
684
+ "use client"
685
+
686
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
687
+ import {
688
+ createSliderController,
689
+ Slider,
690
+ SliderControl,
691
+ SliderIndicator,
692
+ SliderThumb,
693
+ SliderTrack,
694
+ SliderValue,
695
+ } from "@ui-sdk/react/slider"
696
+ import * as React from "react"
697
+ import { useLog } from "./log"
698
+
699
+ function Example() {
700
+ const log = useLog()
701
+ const [volume] = React.useState(() => createSliderController<number>())
702
+
703
+ return (
704
+ <div data-slot="stack">
705
+ <div data-slot="controls">
706
+ <button type="button" onClick={() => volume.setValue(0)}>
707
+ Mute
708
+ </button>
709
+ <button
710
+ type="button"
711
+ onClick={() => volume.setValue(volume.getValue() + 10)}
712
+ >
713
+ Louder
714
+ </button>
715
+ <button
716
+ type="button"
717
+ onClick={() => log(`Volume is ${volume.getValue()}`)}
718
+ >
719
+ Read
720
+ </button>
721
+ <button type="button" onClick={() => volume.focus()}>
722
+ Focus
723
+ </button>
724
+ </div>
725
+ <Field>
726
+ <Slider
727
+ defaultValue={40}
728
+ controller={volume}
729
+ onValueChange={(value, details) =>
730
+ log(`Volume ${value} (${details.reason})`)
731
+ }
732
+ >
733
+ <div data-slot="row">
734
+ <FieldLabel>Volume</FieldLabel>
735
+ <SliderValue />
736
+ </div>
737
+ <SliderControl>
738
+ <SliderTrack>
739
+ <SliderIndicator />
740
+ <SliderThumb />
741
+ </SliderTrack>
742
+ </SliderControl>
743
+ </Slider>
744
+ </Field>
745
+ </div>
746
+ )
747
+ }
748
+
749
+ export { Example }
750
+ ```
751
+
752
+ Use Mute, Louder and Focus to operate Volume from outside the slider. The console logs each change with the imperative reason, and Read logs the value.
753
+
754
+ ## Form
755
+
756
+ A hidden input beside the slider is what the form sees. It takes the slider's `name`, `form` and `disabled`, so `FormData`, `form.reset()`, a disabled `<fieldset>` and the browser's restore all work.
757
+
758
+ - **Submit:** a single thumb submits its number under `name`, like `40`. A range submits `"start/end"`, like `200/800`. Without a name, it submits nothing.
759
+ - **Reset:** a form reset returns it to `defaultValue`, with the `reset` reason.
760
+ - **Join a form from elsewhere:** set `form` to the id of a form.
761
+
762
+ `required` adds `data-required`. A slider always has a value, so it never blocks a form.
763
+
764
+ ```tsx title="form.tsx"
765
+ "use client"
766
+
767
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
768
+ import {
769
+ Slider,
770
+ SliderControl,
771
+ SliderIndicator,
772
+ SliderThumb,
773
+ SliderTrack,
774
+ SliderValue,
775
+ } from "@ui-sdk/react/slider"
776
+ import * as React from "react"
777
+ import { useLog } from "./log"
778
+
779
+ function Example() {
780
+ const log = useLog()
781
+ const form = React.useId()
782
+
783
+ return (
784
+ <div data-slot="stack">
785
+ <form
786
+ id={form}
787
+ data-slot="stack"
788
+ onSubmit={(event) => {
789
+ event.preventDefault()
790
+ const data = [...new FormData(event.currentTarget)].map(
791
+ ([name, value]) => `${name}=${value}`
792
+ )
793
+ log(`Submitted ${data.join(", ")}`)
794
+ }}
795
+ >
796
+ <Field>
797
+ <Slider name="price" defaultValue={[200, 800]} max={1000} step={10}>
798
+ <div data-slot="row">
799
+ <FieldLabel>Price</FieldLabel>
800
+ <SliderValue />
801
+ </div>
802
+ <SliderControl>
803
+ <SliderTrack>
804
+ <SliderIndicator />
805
+ <SliderThumb index={0} aria-label="Minimum price" />
806
+ <SliderThumb index={1} aria-label="Maximum price" />
807
+ </SliderTrack>
808
+ </SliderControl>
809
+ </Slider>
810
+ </Field>
811
+ <Field>
812
+ <Slider name="rating" defaultValue={3} min={1} max={5}>
813
+ <div data-slot="row">
814
+ <FieldLabel>Minimum rating</FieldLabel>
815
+ <SliderValue />
816
+ </div>
817
+ <SliderControl>
818
+ <SliderTrack>
819
+ <SliderIndicator />
820
+ <SliderThumb />
821
+ </SliderTrack>
822
+ </SliderControl>
823
+ </Slider>
824
+ </Field>
825
+ </form>
826
+ <Field>
827
+ <Slider
828
+ name="distance"
829
+ form={form}
830
+ defaultValue={25}
831
+ max={100}
832
+ step={5}
833
+ format={{ style: "unit", unit: "kilometer" }}
834
+ >
835
+ <div data-slot="row">
836
+ <FieldLabel>Distance</FieldLabel>
837
+ <SliderValue />
838
+ </div>
839
+ <SliderControl>
840
+ <SliderTrack>
841
+ <SliderIndicator />
842
+ <SliderThumb />
843
+ </SliderTrack>
844
+ </SliderControl>
845
+ </Slider>
846
+ </Field>
847
+ <div data-slot="row">
848
+ <button type="submit" form={form} data-variant="solid">
849
+ Save
850
+ </button>
851
+ <button type="reset" form={form}>
852
+ Reset
853
+ </button>
854
+ </div>
855
+ </div>
856
+ )
857
+ }
858
+
859
+ export { Example }
860
+ ```
861
+
862
+ Adjust the filters and press Save to log the submitted values. Distance sits outside the form and joins it by id. Reset restores the defaults.
863
+
864
+ ## Read-Only
865
+
866
+ Set `readOnly` to keep the slider focusable and submitted, while drags, keys and screen reader adjustments leave its value alone.
867
+
868
+ ```tsx
869
+ <Field>
870
+ <FieldLabel>Storage</FieldLabel>
871
+ <Slider name="storage" defaultValue={50} readOnly>
872
+ <SliderControl>
873
+ <SliderTrack>
874
+ <SliderIndicator />
875
+ <SliderThumb />
876
+ </SliderTrack>
877
+ </SliderControl>
878
+ </Slider>
879
+ <FieldDescription>Set by your plan.</FieldDescription>
880
+ </Field>
881
+ ```
882
+
883
+ ## Custom Rendering
884
+
885
+ Every part takes `render`, to render a different element or component in its place. `render`, `className` and `children` can be functions of the part's state, like whether a thumb is being dragged, its `thumbValue` and its `percent` along the track. Here the thumb's children add a bubble with its value, and `SliderValue`'s children add a percent sign.
886
+
887
+ ```tsx title="render.tsx"
888
+ "use client"
889
+
890
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
891
+ import {
892
+ Slider,
893
+ SliderControl,
894
+ SliderIndicator,
895
+ SliderThumb,
896
+ SliderTrack,
897
+ SliderValue,
898
+ } from "@ui-sdk/react/slider"
899
+
900
+ function Example() {
901
+ return (
902
+ <Field>
903
+ <Slider defaultValue={60}>
904
+ <div data-slot="row">
905
+ <FieldLabel>Brightness</FieldLabel>
906
+ <SliderValue>{(state) => `${state.text}%`}</SliderValue>
907
+ </div>
908
+ <SliderControl>
909
+ <SliderTrack>
910
+ <SliderIndicator />
911
+ <SliderThumb render={<span />}>
912
+ {(state) => <span data-slot="output">{state.thumbValue}</span>}
913
+ </SliderThumb>
914
+ </SliderTrack>
915
+ </SliderControl>
916
+ </Slider>
917
+ </Field>
918
+ )
919
+ }
920
+
921
+ export { Example }
922
+ ```
923
+
924
+ Focus or drag the Brightness thumb to show its value above it.
925
+
926
+ See [Parts and Composition](../parts-and-composition.md) for the rendering contract and [State](#state) for each part's fields.
927
+
928
+ ## Styling
929
+
930
+ Parts are unstyled, apart from the positions they need: thumbs and the indicator get `position: absolute` and their place along the track in percent, as inline styles. Give the control some height around a thin track, the track a size and a background, and each thumb a size. The thumb's range input is what takes focus, so draw the focus ring on the thumb with `:has(:focus-visible)`, and style a drag with `data-dragging`.
931
+
932
+ Each part has a `data-slot`, such as `slider-thumb`, and its state attributes are listed under [Data Attributes](#data-attributes).
933
+
934
+ The indicator and thumbs also write CSS variables, for anything else that should follow them, like a label above a thumb.
935
+
936
+ ### SliderIndicator
937
+ - `--slider-indicator-start`: Where the indicator starts along the track, as a percentage. 0% for a single thumb.
938
+ - `--slider-indicator-size`: How much of the track the indicator covers, as a percentage.
939
+
940
+ ### SliderThumb
941
+ - `--slider-thumb-position`: Where the thumb sits along the track, as a percentage from the start. Use it for a label that follows the thumb.
942
+
943
+ ## Data Attributes
944
+
945
+ ### Slider
946
+ `[data-slot="slider"]`
947
+
948
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
949
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
950
+ - `data-dragging`: Present while a thumb is being dragged.
951
+
952
+ ### SliderValue
953
+ `[data-slot="slider-value"]`
954
+
955
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
956
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
957
+ - `data-dragging`: Present while a thumb is being dragged.
958
+
959
+ ### SliderControl
960
+ `[data-slot="slider-control"]`
961
+
962
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
963
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
964
+ - `data-dragging`: Present while a thumb is being dragged.
965
+
966
+ ### SliderTrack
967
+ `[data-slot="slider-track"]`
968
+
969
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
970
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
971
+ - `data-dragging`: Present while a thumb is being dragged.
972
+
973
+ ### SliderIndicator
974
+ `[data-slot="slider-indicator"]`
975
+
976
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
977
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
978
+ - `data-dragging`: Present while a thumb is being dragged.
979
+
980
+ ### SliderThumb
981
+ `[data-slot="slider-thumb"]`
982
+
983
+ - `data-orientation` (`horizontal`, `vertical`): The slider's axis.
984
+ - `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
985
+ - `data-index` (`0`, `1`): Which value the thumb shows, from 0.
986
+ - `data-dragging`: Present while this thumb is being dragged.
987
+
988
+ ## API Reference
989
+
990
+ Props and callbacks are the same as below, as React props.
991
+
992
+ ### Slider
993
+
994
+ - `value?` (`Value`): The value: a number for one thumb, or an array with one number per thumb, like `[20, 80]` for a range. Pass it to control the slider.
995
+ - `defaultValue?` (`Value`): The value at first, when uncontrolled. A form reset returns to it.
996
+ - `onValueChange?` (`((value: Value, details: SliderValueChangeDetails) => void)`): Runs while the value changes: on every step of a drag or a key. `details.thumb` says which thumb moved, and `details.cancel()` stops it.
997
+ - `onValueCommit?` (`((value: Value, details: SliderValueCommitDetails) => void)`): Runs once an interaction that changed the value ends: the pointer lifts, the key comes up, or a screen reader adjusts it. For saving.
998
+ - `min?` (`number`): The lowest value.
999
+ - `max?` (`number`): The highest value. When the steps from `min` don't land on it, the highest step below it is the end.
1000
+ - `step?` (`number`): How far one step moves, from `min`.
1001
+ - `largeStep?` (`number`): How far Page Up, Page Down and Shift with an arrow key move.
1002
+ - `minStepsBetweenThumbs?` (`number`): How many steps apart neighbouring thumbs stay. Thumbs never pass each other.
1003
+ - `orientation?` (`"horizontal" | "vertical"`): The slider's axis. Vertical sliders grow upward.
1004
+ - `disabled?` (`boolean`): Disables it like a native control: thumbs can't be focused or moved, and nothing is submitted.
1005
+ - `readOnly?` (`boolean`): Keeps it focusable and submitted, but stops drags, keys and screen reader adjustments from changing it.
1006
+ - `required?` (`boolean`): Marks it required, with `data-required`. A slider always has a value, so it never blocks a form.
1007
+ - `name?` (`string`): The name it submits under: the number, or a range as `"start/end"`. Without one, it submits nothing.
1008
+ - `form?` (`string`): The id of a form to belong to, when it isn't inside that form.
1009
+ - `format?` (`NumberFormat`): How values read, for screen readers and `SliderValue`: `Intl.NumberFormat` options, like `{ style: "currency", currency: "EUR" }`, or a function of the value.
1010
+ - `locale?` (`string`): The locale values are formatted in. Defaults to the page's `lang`, or the browser's language without one, once the page is interactive. Server markup uses English until then, so it never disagrees with the client.
1011
+ - `controller?` (`{ getValue: () => Value; setValue(value: Value, options?: SliderChangeOptions): boolean; focus: (thumb?: number) => void; }`): Moves the slider from anywhere. Create one with `createSliderController()`.
1012
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
1013
+
1014
+ ### SliderThumb
1015
+
1016
+ - `index?` (`number`): Which value the thumb shows, from 0. Give each thumb of a range its own.
1017
+
1018
+ ### State
1019
+
1020
+ What render functions, state-based class names and slots receive. Every part gets the slider's state.
1021
+
1022
+ - `value` (`number | number[]`): The value: a number, or one number per thumb.
1023
+ - `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
1024
+ - `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
1025
+ - `required` (`boolean`): Whether it's marked required.
1026
+ - `dragging` (`boolean`): Whether a thumb is being dragged.
1027
+ - `orientation` (`"horizontal" | "vertical"`): The slider's axis.
1028
+
1029
+ Thumbs add their own fields, and have their own value, `thumbValue`, in place of the whole `value`, so a thumb renders only when its own value moves:
1030
+
1031
+ - `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
1032
+ - `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
1033
+ - `required` (`boolean`): Whether it's marked required.
1034
+ - `dragging` (`boolean`): Whether a thumb is being dragged.
1035
+ - `orientation` (`"horizontal" | "vertical"`): The slider's axis.
1036
+ - `index` (`number`): Which value the thumb shows.
1037
+ - `thumbValue` (`number`): The thumb's own value.
1038
+ - `percent` (`number`): Where the thumb sits, from 0 to 100.
1039
+
1040
+ `SliderValue` adds the formatted text:
1041
+
1042
+ - `value` (`number | number[]`): The value: a number, or one number per thumb.
1043
+ - `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
1044
+ - `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
1045
+ - `required` (`boolean`): Whether it's marked required.
1046
+ - `dragging` (`boolean`): Whether a thumb is being dragged.
1047
+ - `orientation` (`"horizontal" | "vertical"`): The slider's axis.
1048
+ - `text` (`string`): The formatted value, a range joined with an en dash.
1049
+
1050
+ ### Reasons
1051
+
1052
+ `details.reason` says why the value changed.
1053
+
1054
+ - `track-press` (`PointerEvent`): A press on a slider's track or an area's surface moved the nearest thumb there.
1055
+ - `drag` (`PointerEvent`): A slider thumb or a resize handle was dragged, or an item was dragged to a new place in a list.
1056
+ - `step-key` (`KeyboardEvent`): Arrow, Page Up, Page Down, Home or End stepped a slider or a number input.
1057
+ - `accessibility-adjust` (`Event`): A screen reader's adjust gesture, like a swipe up or down on a slider.
1058
+ - `reset` (`none`): A form reset.
1059
+ - `restore` (`none`): The browser restored state, like after going back, or a saved value was read back from storage.
1060
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
1061
+
1062
+ ### Controller
1063
+
1064
+ Create one with `createSliderController()` and pass it as `controller`.
1065
+
1066
+ `setValue()` takes an optional `{ reason, event }`, runs the change callback like a drag, and returns `false` when the change is canceled.
1067
+
1068
+ - `getValue()` (`() => Value`): The value: a number, or one number per thumb.
1069
+ - `setValue(value, options?)` (`(value: Value, options?: SliderChangeOptions | undefined) => boolean`): Sets the value, as a user would. It's clamped, snapped to the step and sorted.
1070
+ - `focus(thumb?)` (`(thumb?: number | undefined) => void`): Moves focus to a thumb, the first by default.
1071
+
1072
+ ### Events
1073
+
1074
+ Callbacks get the new value first, then the details.
1075
+
1076
+ | Prop | On | Receives |
1077
+ | --------------- | -------- | --------------------------------------------------------------------------------------- |
1078
+ | `onValueChange` | `Slider` | `(value, details)`: the new value, the [reason](#reasons) and `details.thumb` |
1079
+ | `onValueCommit` | `Slider` | `(value, details)`: the value once the interaction ends, its reason and `details.thumb` |
1080
+
1081
+ ## Accessibility
1082
+
1083
+ The slider follows the [WAI-ARIA slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/), and the [multi-thumb slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/) for ranges. Each thumb is a native range input, named by a `FieldLabel` or its own `aria-label`, and the slider is a group around them.
1084
+
1085
+ ### Keyboard
1086
+
1087
+ Focus a thumb to use these keys. Tab moves through the thumbs in order.
1088
+
1089
+ | Key | Moves the thumb |
1090
+ | ------------------------------------------ | --------------------------------------------------------------------- |
1091
+ | ArrowRight or ArrowUp | One `step` up. In right to left, ArrowRight moves down. |
1092
+ | ArrowLeft or ArrowDown | One `step` down. In right to left, ArrowLeft moves up. |
1093
+ | Page Up, Page Down, or Shift with an arrow | By `largeStep`, ten steps by default. |
1094
+ | Home or End | To the minimum or maximum, or as far as a neighbouring thumb lets it. |
1095
+
1096
+ ### Tested Behavior
1097
+
1098
+ Every line below is an automated test.
1099
+
1100
+ **Keyboard**
1101
+
1102
+ - Starts at its default value.
1103
+ - ArrowRight and ArrowUp increase it by one step.
1104
+ - ArrowLeft and ArrowDown decrease it by one step.
1105
+ - Page Up and Page Down move by the large step.
1106
+ - Shift with an arrow key moves by the large step.
1107
+ - Home and End go to the minimum and the maximum.
1108
+ - A value never goes past the bounds.
1109
+ - Tab moves through the thumbs in order.
1110
+ - Home and End move a range thumb as far as its neighbour.
1111
+ - A range thumb stops at its neighbour.
1112
+ - A canceled change keeps the value.
1113
+ - A controlled slider follows its value.
1114
+
1115
+ **Pointer**
1116
+
1117
+ - Pressing the track moves the thumb there.
1118
+ - Pressing the track focuses the thumb.
1119
+ - Pressing a range's track moves the nearest thumb.
1120
+ - Dragging a thumb follows the pointer.
1121
+ - Grabbing a thumb off center doesn't move it.
1122
+ - Dragging past the end stops at the bound.
1123
+ - Dragging continues outside the slider.
1124
+ - Dragging right from stacked thumbs moves the upper one.
1125
+ - The value commits once, when the pointer lifts.
1126
+ - A screen reader's adjustment reaches the form as one input and one change.
1127
+ - A drag sends input as it moves, and change once when the pointer lifts.
1128
+ - Pressing a thumb without moving commits nothing.
1129
+ - A press on the track commits.
1130
+ - The thumb sits at its value along the track.
1131
+
1132
+ **Vertical**
1133
+
1134
+ - It says it's vertical.
1135
+ - ArrowUp increases it and ArrowDown decreases it.
1136
+ - The bottom is the minimum.
1137
+ - Dragging up increases it.
1138
+
1139
+ **Value and state**
1140
+
1141
+ - The value part shows the value, and a range joined with a dash.
1142
+ - The value reads in the format and the locale.
1143
+ - A disabled slider can't be focused or changed.
1144
+ - Its parts say when it's dragged.
1145
+ - A single slider submits its value under its name.
1146
+
1147
+ Runs in React, Vue and web components.
1148
+
1149
+ Beyond the pattern:
1150
+
1151
+ - A dragged range thumb stops at its neighbour, as the APG's multi-thumb slider says.
1152
+ - Each range thumb's own range ends at its neighbour.
1153
+ - A form reset returns every slider to its default.
1154
+ - Holding an arrow key commits once, when the key comes up.
1155
+ - Dragging left from stacked thumbs moves the lower one.
1156
+ - Pressing the track beside stacked thumbs moves the one on that side.
1157
+ - A key moves a thumb as close to its neighbour as the minimum distance allows.
1158
+ - Touch drags on the control never scroll the page.
1159
+ - A range submits one start/end value.
1160
+ - Clicking a range's label focuses its first thumb.
1161
+
1162
+ Runs in React, Vue and web components.
1163
+
1164
+ Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
1165
+
1166
+ - Announcements while moving to a slider with a keyboard.
1167
+ - A range thumb announces how far it can go and its formatted value.
1168
+
1169
+ Runs in React, Vue and web components.