@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,220 @@
1
+ # Preferences
2
+
3
+ Apply user preferences to data attributes so your styles can follow them.
4
+
5
+ `Preferences` puts your preferences on `data-*` attributes and keeps them up to date. Use it around a part of your app, or apply a preference to the whole page.
6
+
7
+ ```tsx title="basic.tsx"
8
+ "use client"
9
+
10
+ import { createLocalStorage, createPreference } from "@ui-sdk/core"
11
+ import { Button } from "@ui-sdk/react/button"
12
+ import { Preferences, usePreference } from "@ui-sdk/react/preferences"
13
+ import { useLog } from "./log"
14
+
15
+ const view = createPreference({
16
+ values: ["list", "grid"],
17
+ defaultValue: "list",
18
+ attribute: "data-view",
19
+ storage: createLocalStorage("ui-sdk-preferences-view"),
20
+ })
21
+
22
+ const files = [
23
+ { name: "Q3 roadmap.pdf", detail: "2.4 MB" },
24
+ { name: "Launch checklist.md", detail: "12 KB" },
25
+ { name: "Offsite photos", detail: "24 items" },
26
+ { name: "Brand assets.zip", detail: "18 MB" },
27
+ ]
28
+
29
+ function Example() {
30
+ const log = useLog()
31
+ const { value } = usePreference(view)
32
+
33
+ const choose = (next: "list" | "grid", event: React.MouseEvent) => {
34
+ view.set(next, { reason: "press", event: event.nativeEvent })
35
+ log(`Saved the ${next} view`)
36
+ }
37
+
38
+ return (
39
+ <Preferences preferences={[view]}>
40
+ <div data-slot="stack">
41
+ <div data-slot="controls" role="group" aria-label="View">
42
+ <Button
43
+ aria-pressed={value === "list"}
44
+ onClick={(event) => choose("list", event)}
45
+ >
46
+ List
47
+ </Button>
48
+ <Button
49
+ aria-pressed={value === "grid"}
50
+ onClick={(event) => choose("grid", event)}
51
+ >
52
+ Grid
53
+ </Button>
54
+ </div>
55
+ <ul data-slot="files" aria-label="Files">
56
+ {files.map((file) => (
57
+ <li key={file.name}>
58
+ <span>{file.name}</span>
59
+ <span data-slot="hint">{file.detail}</span>
60
+ </li>
61
+ ))}
62
+ </ul>
63
+ </div>
64
+ </Preferences>
65
+ )
66
+ }
67
+
68
+ export { Example }
69
+ ```
70
+
71
+ Choose Grid, then reload the page. The files come back in the view you chose, and the console logs each change.
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ npm install @ui-sdk/react @ui-sdk/core
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ Define the preference you want to apply. Here, `view` chooses how a file list is laid out:
82
+
83
+ ```ts title="preferences.ts"
84
+ import { createPreference } from "@ui-sdk/core"
85
+
86
+ const view = createPreference({
87
+ values: ["list", "grid"],
88
+ defaultValue: "list",
89
+ attribute: "data-view",
90
+ })
91
+
92
+ export { view }
93
+ ```
94
+
95
+ Pass it to `Preferences`, then style the content by its attribute:
96
+
97
+ ```tsx title="file-list.tsx"
98
+ import { Preferences } from "@ui-sdk/react/preferences"
99
+ import { view } from "./preferences"
100
+
101
+ function FileList() {
102
+ return (
103
+ <Preferences preferences={[view]}>
104
+ <ul data-slot="files">
105
+ <li>Q3 roadmap.pdf</li>
106
+ <li>Launch checklist.md</li>
107
+ </ul>
108
+ </Preferences>
109
+ )
110
+ }
111
+
112
+ export { FileList }
113
+ ```
114
+
115
+ ```css
116
+ [data-view="grid"] [data-slot="files"] {
117
+ display: grid;
118
+ grid-template-columns: repeat(2, 1fr);
119
+ }
120
+ ```
121
+
122
+ `Preferences` starts with `data-view="list"`. Calling `view.set("grid")` updates it, and the list becomes a grid. `Preferences` renders with `display: contents`, so it adds no box of its own.
123
+
124
+ ## Save the Value
125
+
126
+ Add `storage` to save the choice between visits:
127
+
128
+ ```ts title="preferences.ts"
129
+ import { createLocalStorage, createPreference } from "@ui-sdk/core"
130
+
131
+ const view = createPreference({
132
+ values: ["list", "grid"],
133
+ defaultValue: "list",
134
+ attribute: "data-view",
135
+ storage: createLocalStorage("ui-sdk-preferences-view"),
136
+ })
137
+
138
+ export { view }
139
+ ```
140
+
141
+ The preference now saves changes in local storage and restores the saved value when the page reloads. You can also [use session storage or cookies with `createPreference`](../core.md#remember-the-choice).
142
+
143
+ ## Read the Value
144
+
145
+ Use `usePreference` when the value affects what your component renders:
146
+
147
+ ```tsx title="view-switch.tsx"
148
+ import { usePreference } from "@ui-sdk/react/preferences"
149
+ import { view } from "./preferences"
150
+
151
+ function ViewSwitch() {
152
+ const { value } = usePreference(view)
153
+
154
+ return (
155
+ <button
156
+ aria-pressed={value === "grid"}
157
+ onClick={() => view.set(value === "grid" ? "list" : "grid")}
158
+ >
159
+ Grid
160
+ </button>
161
+ )
162
+ }
163
+
164
+ export { ViewSwitch }
165
+ ```
166
+
167
+ Pass the same preference instance wherever you need its value. `usePreference` also works outside a `Preferences` component.
168
+
169
+ ## Apply to the Whole Page
170
+
171
+ Set `global: true` when defining the preference:
172
+
173
+ ```ts
174
+ const view = createPreference({
175
+ values: ["list", "grid"],
176
+ defaultValue: "list",
177
+ attribute: "data-view",
178
+ global: true,
179
+ storage: createLocalStorage("ui-sdk-preferences-view"),
180
+ })
181
+ ```
182
+
183
+ Keep passing it to `Preferences`. Its attribute now goes on `<html>`, so styles throughout the page can use it. A global preference needs only one mounted `Preferences` component to apply it.
184
+
185
+ ## Multiple Preferences
186
+
187
+ Pass more than one preference in the list. For example, you can apply `view` alongside a [color scheme](../core.md#choose-a-color-scheme):
188
+
189
+ ```tsx
190
+ <Preferences preferences={[view, colorScheme]}>
191
+ <App />
192
+ </Preferences>
193
+ ```
194
+
195
+ Each preference uses its own attribute and `global` option. Wrappers that share a preference instance follow the same value. For independent choices, use separate preference instances and different storage keys.
196
+
197
+ ## Server Rendering
198
+
199
+ On the server, `Preferences` includes a script that applies saved values before the page paints. CSS can use those attributes immediately. `usePreference` starts with the server's value and updates after hydration.
200
+
201
+ Follow the [server rendering setup](../server-rendering.md#saved-preferences-and-storage) to place `Preferences` in your app and keep cookie-backed preferences separate for each request.
202
+
203
+ If your Content Security Policy requires a nonce, pass the nonce from your response:
204
+
205
+ ```tsx
206
+ <Preferences preferences={[view]} nonce={nonce}>
207
+ <App />
208
+ </Preferences>
209
+ ```
210
+
211
+ ## API Reference
212
+
213
+ ### Preferences
214
+
215
+ - `nonce?` (`string`): The nonce your Content Security Policy gives inline scripts. Example: [Server Rendering](#server-rendering).
216
+ - `preferences` (`readonly PreferenceLike[]`): The preferences to show, from `createPreference` or `createColorScheme`. Example: [Usage](#usage).
217
+
218
+ ### usePreference
219
+
220
+ `usePreference(preference)` returns the preference's current state and updates the component when it changes. For a preference created with `createPreference`, the state is `{ value }`. For a color scheme, it also includes `system` and `resolved`.
@@ -0,0 +1,443 @@
1
+ # Progress
2
+
3
+ Show a task’s progress, or an indeterminate state while its progress is unknown.
4
+
5
+ ```tsx title="basic.tsx"
6
+ import {
7
+ Progress,
8
+ ProgressIndicator,
9
+ ProgressLabel,
10
+ ProgressTrack,
11
+ ProgressValue,
12
+ } from "@ui-sdk/react/progress"
13
+
14
+ function Example() {
15
+ return (
16
+ <Progress value={60}>
17
+ <ProgressLabel>Exporting video</ProgressLabel>
18
+ <ProgressValue />
19
+ <ProgressTrack>
20
+ <ProgressIndicator />
21
+ </ProgressTrack>
22
+ </Progress>
23
+ )
24
+ }
25
+
26
+ export { Example }
27
+ ```
28
+
29
+ Read the export bar. Its label and 60% value are available both visually and to a screen reader.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install @ui-sdk/react @ui-sdk/core
35
+ ```
36
+
37
+ ## Composition
38
+
39
+ Use the following composition to build a `Progress`:
40
+
41
+ ```text
42
+ Progress
43
+ ├── ProgressLabel
44
+ ├── ProgressValue
45
+ └── ProgressTrack
46
+ └── ProgressIndicator
47
+ ```
48
+
49
+ - `Progress` holds the value and range.
50
+ - `ProgressLabel` gives it a visible name. You can use `aria-label` on the root instead.
51
+ - `ProgressValue` displays the formatted value.
52
+ - `ProgressTrack` gives the bar its size, and `ProgressIndicator` fills it.
53
+
54
+ The value is already part of the root's accessible description. The visible value is hidden from screen readers to avoid reading it twice.
55
+
56
+ ## Usage
57
+
58
+ Import the parts and put them together:
59
+
60
+ ```tsx
61
+ <Progress value={40}>
62
+ <ProgressLabel />
63
+ <ProgressValue />
64
+ <ProgressTrack>
65
+ <ProgressIndicator />
66
+ </ProgressTrack>
67
+ </Progress>
68
+ ```
69
+
70
+ ## Build Your Own
71
+
72
+ `@ui-sdk/react/progress` comes with a ready-made Progress. If you'd like to build your own, with your own names, start from the `progress` primitive in `@ui-sdk/core`:
73
+
74
+ ```ts title="upload.ts"
75
+ "use client"
76
+
77
+ import { progress } from "@ui-sdk/core/progress"
78
+ import { createParts } from "@ui-sdk/react/primitive"
79
+
80
+ const {
81
+ Root: Upload,
82
+ Label: UploadLabel,
83
+ Value: UploadValue,
84
+ Track: UploadTrack,
85
+ Indicator: UploadBar,
86
+ } = createParts(progress, {
87
+ root: "div",
88
+ label: "span",
89
+ value: "span",
90
+ track: "div",
91
+ indicator: "div",
92
+ })
93
+
94
+ export { Upload, UploadBar, UploadLabel, UploadTrack, UploadValue }
95
+ ```
96
+
97
+ ```tsx
98
+ <Upload value={40}>
99
+ <UploadLabel>Uploading photos</UploadLabel>
100
+ <UploadValue>{(state) => state.text}</UploadValue>
101
+ <UploadTrack>
102
+ <UploadBar />
103
+ </UploadTrack>
104
+ </Upload>
105
+ ```
106
+
107
+ `ProgressValue` shows the formatted value when it has no children. A part from `createParts` renders only its children, so pass `(state) => state.text` to show the same text.
108
+
109
+ Each part keeps the behavior, state and data attributes of its ready-made counterpart: the root is the `progressbar`, and the indicator sets its width from the value. See [Rendering](../rendering.md) and [Custom Parts](../custom-parts.md) for more.
110
+
111
+ ## Indeterminate
112
+
113
+ Leave out `value` when you do not know how much work is complete. Every part gets `data-indeterminate`, and the root has no `aria-valuenow`.
114
+
115
+ The indicator has no inline width in this state. This example sweeps a short highlight across the track, and shows a still, full-width fill when reduced motion is on.
116
+
117
+ ```tsx title="indeterminate.tsx"
118
+ import {
119
+ Progress,
120
+ ProgressIndicator,
121
+ ProgressLabel,
122
+ ProgressTrack,
123
+ } from "@ui-sdk/react/progress"
124
+
125
+ function Example() {
126
+ return (
127
+ <Progress value={null}>
128
+ <ProgressLabel>Connecting to server</ProgressLabel>
129
+ <ProgressTrack>
130
+ <ProgressIndicator />
131
+ </ProgressTrack>
132
+ </Progress>
133
+ )
134
+ }
135
+
136
+ export { Example }
137
+ ```
138
+
139
+ Read the Connecting to server bar. It shows an ongoing task without announcing a percentage.
140
+
141
+ ## Changing the Value
142
+
143
+ Pass the latest `value` as your task progresses. The parts get `data-progressing` while the value is below `max`, and `data-complete` when it reaches the end.
144
+
145
+ ```tsx title="controlled.tsx"
146
+ "use client"
147
+
148
+ import {
149
+ Progress,
150
+ ProgressIndicator,
151
+ ProgressLabel,
152
+ ProgressTrack,
153
+ ProgressValue,
154
+ } from "@ui-sdk/react/progress"
155
+ import * as React from "react"
156
+ import { useLog } from "./log"
157
+
158
+ const total = 5
159
+
160
+ function Example() {
161
+ const log = useLog()
162
+ const [uploaded, setUploaded] = React.useState(1)
163
+
164
+ function uploadNext() {
165
+ if (uploaded >= total) {
166
+ return
167
+ }
168
+ setUploaded(uploaded + 1)
169
+ log(`Uploaded ${uploaded + 1} of ${total} photos`)
170
+ }
171
+
172
+ function reset() {
173
+ setUploaded(0)
174
+ log("Reset to 0 photos")
175
+ }
176
+
177
+ return (
178
+ <div data-slot="stack">
179
+ <div data-slot="controls">
180
+ <button
181
+ type="button"
182
+ aria-disabled={uploaded >= total}
183
+ onClick={uploadNext}
184
+ >
185
+ Upload Next
186
+ </button>
187
+ <button type="button" onClick={reset}>
188
+ Reset
189
+ </button>
190
+ </div>
191
+ <Progress value={(uploaded / total) * 100}>
192
+ <ProgressLabel>Uploading photos</ProgressLabel>
193
+ <ProgressValue />
194
+ <ProgressTrack>
195
+ <ProgressIndicator />
196
+ </ProgressTrack>
197
+ </Progress>
198
+ </div>
199
+ )
200
+ }
201
+
202
+ export { Example }
203
+ ```
204
+
205
+ Click Upload Next to add one photo, or 20%, and watch the console count them. At 100%, the button stops advancing the bar. Click Reset to start at 0%.
206
+
207
+ Progress displays your state; it has no `onValueChange` callback.
208
+
209
+ ## Bounds
210
+
211
+ `min` and `max` default to 0 and 100. A value outside that range is clamped for the fill, the percentage and `aria-valuenow`, but the state's `value` and a format other than percent keep it as you passed it. A value of 3 between 1 and 5 fills half the track.
212
+
213
+ ```tsx title="min-max.tsx"
214
+ import {
215
+ Progress,
216
+ ProgressIndicator,
217
+ ProgressLabel,
218
+ ProgressTrack,
219
+ ProgressValue,
220
+ } from "@ui-sdk/react/progress"
221
+
222
+ function Example() {
223
+ return (
224
+ <Progress value={3} min={1} max={5}>
225
+ <ProgressLabel>Account setup, step 3 of 5</ProgressLabel>
226
+ <ProgressValue />
227
+ <ProgressTrack>
228
+ <ProgressIndicator />
229
+ </ProgressTrack>
230
+ </Progress>
231
+ )
232
+ }
233
+
234
+ export { Example }
235
+ ```
236
+
237
+ Compare step 3 of 5 with the fill. Because the range starts at 1, the value is halfway through the range.
238
+
239
+ ## Format and Locale
240
+
241
+ Pass `Intl.NumberFormat` options to `format` for units or decimal places. A function can return custom text, such as the time remaining.
242
+
243
+ ```tsx title="format.tsx"
244
+ import {
245
+ Progress,
246
+ ProgressIndicator,
247
+ ProgressLabel,
248
+ ProgressTrack,
249
+ ProgressValue,
250
+ } from "@ui-sdk/react/progress"
251
+
252
+ const bars = [
253
+ {
254
+ label: "Download",
255
+ value: 120,
256
+ max: 250,
257
+ format: { style: "unit", unit: "gigabyte" },
258
+ },
259
+ {
260
+ label: "Video render",
261
+ value: 87.25,
262
+ format: { style: "percent", maximumFractionDigits: 1 },
263
+ },
264
+ {
265
+ label: "Backup",
266
+ value: 45,
267
+ max: 60,
268
+ format: (minutes: number) => `${60 - minutes} min left`,
269
+ },
270
+ { label: "Synchronisierung", value: 40, locale: "de-DE" },
271
+ ] satisfies {
272
+ label: string
273
+ value: number
274
+ max?: number
275
+ format?: Intl.NumberFormatOptions | ((value: number) => string)
276
+ locale?: string
277
+ }[]
278
+
279
+ function Example() {
280
+ return (
281
+ <div data-slot="stack">
282
+ {bars.map(({ label, ...options }) => (
283
+ <Progress key={label} {...options}>
284
+ <ProgressLabel>{label}</ProgressLabel>
285
+ <ProgressValue />
286
+ <ProgressTrack>
287
+ <ProgressIndicator />
288
+ </ProgressTrack>
289
+ </Progress>
290
+ ))}
291
+ </div>
292
+ )
293
+ }
294
+
295
+ export { Example }
296
+ ```
297
+
298
+ Compare the progress readings. They show downloaded data, a precise percentage, time remaining, and a localized percentage.
299
+
300
+ A percent format reports the value's position between `min` and `max`. Other formats use the value itself.
301
+
302
+ `locale` controls digits and separators. If you omit it, the component reads the page's language after it mounts, with the browser's language as a fallback. The initial server and client render use English.
303
+
304
+ Set `dir="rtl"` on the surrounding content to fill the track from the right.
305
+
306
+ ## Value Label
307
+
308
+ Use `getValueLabel` to give screen readers more context than the visible number. Its details include `formatted`, `percent`, `min`, and `max`. Handle a `null` value when the task can be indeterminate; `percent` is also `null` and `formatted` is empty then.
309
+
310
+ ```tsx title="value-label.tsx"
311
+ import {
312
+ Progress,
313
+ ProgressIndicator,
314
+ ProgressLabel,
315
+ ProgressTrack,
316
+ ProgressValue,
317
+ } from "@ui-sdk/react/progress"
318
+
319
+ function Example() {
320
+ return (
321
+ <Progress
322
+ value={3}
323
+ max={12}
324
+ getValueLabel={(value, details) =>
325
+ `${value} of ${details.max} files, ${details.formatted}`
326
+ }
327
+ >
328
+ <ProgressLabel>Copying files</ProgressLabel>
329
+ <ProgressValue>3 of 12</ProgressValue>
330
+ <ProgressTrack>
331
+ <ProgressIndicator />
332
+ </ProgressTrack>
333
+ </Progress>
334
+ )
335
+ }
336
+
337
+ export { Example }
338
+ ```
339
+
340
+ Read Copying files with a screen reader. It announces 3 of 12 files and 25%, while the visible value stays compact.
341
+
342
+ Use [Meter](./meter.md) for a reading such as storage used, rather than a task that will finish.
343
+
344
+ ## Custom Rendering
345
+
346
+ See [Parts and Composition](../parts-and-composition.md) for changing a part's element or reading its state. Use `format` for text that should be shared by the visible value and screen readers.
347
+
348
+ ## Styling
349
+
350
+ Give the track a height, background, and width. The indicator sets its width as an inline percentage of the track. Style `data-indeterminate` for a task whose progress is unknown, since the indicator has no width then, and `data-complete` once the task finishes. Both styling options are shown in each example's Code tab.
351
+
352
+ Each part has a `data-slot`, such as `progress-indicator`, and its state attributes are listed under [Data Attributes](#data-attributes).
353
+
354
+ ### Progress
355
+ The root exposes a CSS variable for positioning other content alongside the reading.
356
+
357
+ - `--progress-value`: Where the value sits between `min` and `max`, as a percentage, like 40%. Unset while indeterminate.
358
+
359
+ ## Data Attributes
360
+
361
+ ### Progress
362
+ `[data-slot="progress"]`
363
+
364
+ - `data-indeterminate`: Present while the value is null: the task is under way, but how far is unknown.
365
+ - `data-progressing`: Present while the value is below `max`.
366
+ - `data-complete`: Present once the value reaches `max`.
367
+
368
+ ### ProgressLabel
369
+ `[data-slot="progress-label"]`
370
+
371
+ - `data-indeterminate`: Present while the value is null: the task is under way, but how far is unknown.
372
+ - `data-progressing`: Present while the value is below `max`.
373
+ - `data-complete`: Present once the value reaches `max`.
374
+
375
+ ### ProgressValue
376
+ `[data-slot="progress-value"]`
377
+
378
+ - `data-indeterminate`: Present while the value is null: the task is under way, but how far is unknown.
379
+ - `data-progressing`: Present while the value is below `max`.
380
+ - `data-complete`: Present once the value reaches `max`.
381
+
382
+ ### ProgressTrack
383
+ `[data-slot="progress-track"]`
384
+
385
+ - `data-indeterminate`: Present while the value is null: the task is under way, but how far is unknown.
386
+ - `data-progressing`: Present while the value is below `max`.
387
+ - `data-complete`: Present once the value reaches `max`.
388
+
389
+ ### ProgressIndicator
390
+ `[data-slot="progress-indicator"]`
391
+
392
+ - `data-indeterminate`: Present while the value is null: the task is under way, but how far is unknown.
393
+ - `data-progressing`: Present while the value is below `max`.
394
+ - `data-complete`: Present once the value reaches `max`.
395
+
396
+ ## API Reference
397
+
398
+ Props are the same as below, as React props.
399
+
400
+ ### Progress
401
+
402
+ - `min?` (`number`): The lowest value.
403
+ - `max?` (`number`): The highest value.
404
+ - `format?` (`NumberFormat`): How the value reads, for screen readers and the Value part: `Intl.NumberFormat` options, or a function of the value. A percent style shows where the value sits between `min` and `max`; any other style shows the value itself, like `{ style: "unit", unit: "gigabyte" }`.
405
+ - `locale?` (`string`): The locale the value is 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.
406
+ - `getValueLabel?` (`((value: number | null, details: GaugeLabelDetails<number | null>) => string)`): The text screen readers announce for the value, when the formatted value says too little, like "3 of 12 files". It receives the value, null while a progress bar is indeterminate, and details: the `formatted` value, its `percent`, `min` and `max`.
407
+ - `value?` (`number | null`): How far the task has come, between `min` and `max`. Null means it's under way but how far is unknown, and the bar is indeterminate.
408
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
409
+
410
+ ### State
411
+
412
+ What render functions, state-based class names and slots receive. Every part gets the same state.
413
+
414
+ - `value` (`number | null`): The value, or null while indeterminate.
415
+ - `percent` (`number | null`): Where the value sits between `min` and `max`, from 0 to 100, or null while indeterminate.
416
+ - `status` (`ProgressStatus`): Whether the progress is unknown, under way, or done.
417
+
418
+ `ProgressValue` adds the formatted text:
419
+
420
+ - `value` (`number | null`): The value, or null while indeterminate.
421
+ - `percent` (`number | null`): Where the value sits between `min` and `max`, from 0 to 100, or null while indeterminate.
422
+ - `status` (`ProgressStatus`): Whether the progress is unknown, under way, or done.
423
+ - `text` (`string`): The formatted value, like "40%". Empty while indeterminate.
424
+
425
+ ## Accessibility
426
+
427
+ Name the component with `ProgressLabel` or `aria-label`. It exposes the range and formatted value through the `progressbar` role. Use `getValueLabel` when that value needs more explanation.
428
+
429
+ ### Tested Behavior
430
+
431
+ Every line below is an automated test.
432
+
433
+ - A progress bar is named by its label and reads its value.
434
+ - The value part shows the formatted value.
435
+ - The indicator is as wide as the value.
436
+ - A new value updates the bar.
437
+ - Every part says whether the task is under way, done, or unknown.
438
+ - An indeterminate bar has no value.
439
+ - A unit format reads the value itself.
440
+ - A value label replaces the formatted value for screen readers.
441
+ - A value above the maximum reads as the maximum.
442
+
443
+ Runs in React, Vue and web components.