@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,252 @@
1
+ # Avatar
2
+
3
+ Show an image with a fallback while it loads or if it fails.
4
+
5
+ Use an Avatar for a person or a team, like in a comment, a member list or an account menu. It shows initials or an icon until the image loads, and keeps showing them if the image fails.
6
+
7
+ ```tsx title="basic.tsx"
8
+ import { Avatar, AvatarFallback, AvatarImage } from "@ui-sdk/react/avatar"
9
+
10
+ const person = {
11
+ name: "shadcn",
12
+ src: "https://github.com/shadcn.png",
13
+ }
14
+
15
+ export function Example() {
16
+ return (
17
+ <div data-slot="row">
18
+ <Avatar>
19
+ <AvatarImage src={person.src} alt={person.name} />
20
+ <AvatarFallback>
21
+ <span aria-hidden="true">CN</span>
22
+ <span data-slot="visually-hidden">shadcn</span>
23
+ </AvatarFallback>
24
+ </Avatar>
25
+ </div>
26
+ )
27
+ }
28
+ ```
29
+
30
+ The image replaces the initials once it loads. A screen reader can read the person's name in either state.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install @ui-sdk/react @ui-sdk/core
36
+ ```
37
+
38
+ ## Composition
39
+
40
+ Use the following composition to build an `Avatar`:
41
+
42
+ ```text
43
+ Avatar
44
+ ├── AvatarImage
45
+ └── AvatarFallback
46
+ ```
47
+
48
+ - `Avatar` shares the image's loading state with its parts.
49
+ - `AvatarImage` renders the image.
50
+ - `AvatarFallback` shows when no image is loaded.
51
+
52
+ The image is a real `img`, including in server markup. The browser can start fetching it before hydration. The fallback stays visible until the image loads; if loading fails, the fallback remains.
53
+
54
+ ## Usage
55
+
56
+ Import the parts and put them together:
57
+
58
+ ```tsx
59
+ <Avatar>
60
+ <AvatarImage src="/ada.png" alt="Ada Lovelace" />
61
+ <AvatarFallback>AL</AvatarFallback>
62
+ </Avatar>
63
+ ```
64
+
65
+ ## Build Your Own
66
+
67
+ `@ui-sdk/react/avatar` comes with a ready-made Avatar. If you'd like to build your own, with your own names, start from the `avatar` primitive in `@ui-sdk/core`:
68
+
69
+ ```ts title="user-photo.ts"
70
+ "use client"
71
+
72
+ import { avatar } from "@ui-sdk/core/avatar"
73
+ import { createParts } from "@ui-sdk/react/primitive"
74
+
75
+ const {
76
+ Root: UserPhoto,
77
+ Image: UserPhotoImage,
78
+ Fallback: UserPhotoInitials,
79
+ } = createParts(avatar, { root: "span", image: "img", fallback: "span" })
80
+
81
+ export { UserPhoto, UserPhotoImage, UserPhotoInitials }
82
+ ```
83
+
84
+ ```tsx
85
+ <UserPhoto>
86
+ <UserPhotoImage src="https://github.com/shadcn.png" alt="shadcn" />
87
+ <UserPhotoInitials>CN</UserPhotoInitials>
88
+ </UserPhoto>
89
+ ```
90
+
91
+ These are the same parts the ready-made Avatar is made of, with your names. See [Rendering](../rendering.md) and [Custom Rendering](#custom-rendering) for more.
92
+
93
+ ## Fallback Delay
94
+
95
+ <span id="behavior-and-options" />
96
+
97
+ Set `delay` on the fallback to wait before showing it. This avoids flashing initials when an image loads quickly. Without a delay, the fallback appears immediately, including on the server.
98
+
99
+ ```tsx
100
+ <Avatar>
101
+ <AvatarImage src="https://github.com/shadcn.png" alt="shadcn" />
102
+ <AvatarFallback delay={600}>CN</AvatarFallback>
103
+ </Avatar>
104
+ ```
105
+
106
+ ## Image Attributes
107
+
108
+ Pass image attributes such as `srcSet`, `crossOrigin`, and `referrerPolicy` to the image. They work on the same `img` the browser loads. For avatars farther down a page, use `loading="lazy"`.
109
+
110
+ ```tsx
111
+ <Avatar>
112
+ <AvatarImage
113
+ src="https://github.com/shadcn.png"
114
+ srcSet="https://github.com/shadcn.png?size=80 1x, https://github.com/shadcn.png?size=160 2x"
115
+ alt="shadcn"
116
+ crossOrigin="anonymous"
117
+ referrerPolicy="no-referrer"
118
+ loading="lazy"
119
+ />
120
+ <AvatarFallback>CN</AvatarFallback>
121
+ </Avatar>
122
+ ```
123
+
124
+ <span id="render-functions" />
125
+
126
+ ## Interactive Avatar
127
+
128
+ Use a button when clicking an avatar performs an action. Give it an accessible name that stays available while the image loads.
129
+
130
+ ```tsx
131
+ <Avatar
132
+ render={<button type="button" aria-label="shadcn" />}
133
+ onClick={openProfile}
134
+ >
135
+ <AvatarImage src="https://github.com/shadcn.png" alt="" />
136
+ <AvatarFallback aria-hidden>CN</AvatarFallback>
137
+ </Avatar>
138
+ ```
139
+
140
+ [Parts and Composition](../parts-and-composition.md) explains how to change the root element in this framework.
141
+
142
+ ## Loading Status
143
+
144
+ `onLoadingStatusChange` receives the new status and change details. The possible statuses are `loading`, `loaded`, `error`, and `idle`.
145
+
146
+ ```tsx
147
+ <Avatar
148
+ onLoadingStatusChange={(status, details) => {
149
+ if (status === "error") reportBrokenImage(details.reason)
150
+ }}
151
+ >
152
+ <AvatarImage src={user.image} alt={user.name} />
153
+ <AvatarFallback>{user.initials}</AvatarFallback>
154
+ </Avatar>
155
+ ```
156
+
157
+ When you replace a loaded image, the browser keeps it visible while the next source loads. A failed replacement shows the fallback. The change details explain why the status changed; see [Reasons](#reasons).
158
+
159
+ ## Custom Rendering
160
+
161
+ Use [Parts and Composition](../parts-and-composition.md) to replace an element or read its state. Every part receives the loading status, so the fallback can say something different when the image fails:
162
+
163
+ ```tsx
164
+ <Avatar>
165
+ <AvatarImage src={user.image} alt={user.name} />
166
+ <AvatarFallback>
167
+ {(state) => (state.loadingStatus === "error" ? "?" : user.initials)}
168
+ </AvatarFallback>
169
+ </Avatar>
170
+ ```
171
+
172
+ ## Styling
173
+
174
+ Set the root's size and let the image and fallback fill it. The image manages its own hidden styles until it loads; keep those styles intact when changing its element.
175
+
176
+ Each part has a `data-slot`, such as `avatar-fallback`, and shows the loading status through the attributes listed under [Data Attributes](#data-attributes).
177
+
178
+ ## Data Attributes
179
+
180
+ ### Avatar
181
+ `[data-slot="avatar"]`
182
+
183
+ - `data-loading-status` (`idle`, `loading`, `loaded`, `error`): Where the image is: no image yet, loading, loaded, or failed.
184
+
185
+ ### AvatarImage
186
+ `[data-slot="avatar-image"]`
187
+
188
+ - `data-loading-status` (`idle`, `loading`, `loaded`, `error`): Where the image is: no image yet, loading, loaded, or failed.
189
+
190
+ ### AvatarFallback
191
+ `[data-slot="avatar-fallback"]`
192
+
193
+ - `data-loading-status` (`idle`, `loading`, `loaded`, `error`): Where the image is: no image yet, loading, loaded, or failed.
194
+
195
+ ## API Reference
196
+
197
+ Props and callbacks are the same as below, as React props.
198
+
199
+ ### Avatar
200
+
201
+ - `onLoadingStatusChange?` (`((status: AvatarLoadingStatus, details: ChangeDetails<AvatarLoadingStatusChangeReason>) => void)`): Runs when the image's loading status changes: `"loading"` when it gets a source, `"loaded"`, `"error"`, and `"idle"` when it leaves the page. `details.reason` says why, and `details.event` is the image's `load` or `error` event, or undefined when the status was known at once, like for a cached image.
202
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
203
+
204
+ ### AvatarImage
205
+
206
+ The image. Renders a real `img`, so the browser can fetch it before hydration, and accepts its attributes, such as `srcSet`, `crossOrigin` and `referrerPolicy`.
207
+
208
+ ### AvatarFallback
209
+
210
+ - `delay?` (`number`): How long to wait, in milliseconds, before showing the fallback, so a fast image appears without the fallback flashing first. Without one, the fallback shows at once, server markup included.
211
+
212
+ ### State
213
+
214
+ What render functions, state-based class names and slots receive. Every part gets the same state.
215
+
216
+ - `loadingStatus` (`AvatarLoadingStatus`): Where the image is: `"idle"`, `"loading"`, `"loaded"` or `"error"`.
217
+
218
+ ### Reasons
219
+
220
+ `details.reason` says why the status changed.
221
+
222
+ - `image-load` (`Event | undefined`): An image loaded. No event when it was already in the browser's cache.
223
+ - `image-error` (`Event | undefined`): An image failed to load, or has no source. No event when that was known at once.
224
+ - `src-change` (`none`): An image got a new source, or left the page, so its status starts over.
225
+
226
+ ### Events
227
+
228
+ Callbacks get the new status first, then the details.
229
+
230
+ | Prop | On | Receives |
231
+ | ----------------------- | -------- | ----------------------------------------------------------------------------------- |
232
+ | `onLoadingStatusChange` | `Avatar` | `(status, details)`: the image's status, the [reason](#reasons) and `details.event` |
233
+
234
+ ## Accessibility
235
+
236
+ Give the image meaningful `alt` text. Include the person's name in the fallback too, using visually hidden text if you only want to show initials.
237
+
238
+ When a visible name beside the avatar already identifies the person, use an empty `alt` and hide decorative fallback content from screen readers.
239
+
240
+ - An image that loads replaces the fallback.
241
+ - An image that fails keeps the fallback.
242
+ - An avatar without an image shows its fallback.
243
+ - A fallback with a delay waits before showing.
244
+ - The image's loading status is reported.
245
+ - Removing the image brings the fallback back.
246
+ - A new source shows its image once it loads.
247
+ - The image is fetched with its own referrer policy.
248
+ - A cached image doesn't flash the fallback when the avatar mounts again.
249
+ - A new source keeps showing the previous image until it loads.
250
+ - The image is fetched once, even when it can't be cached.
251
+
252
+ Runs in React, Vue and web components.
@@ -0,0 +1,444 @@
1
+ # Button
2
+
3
+ Run an action, show that it's working, and keep it reachable when disabled.
4
+
5
+ Button renders a native `button` and adds what it doesn't do on its own: it stays pending while an async action runs, so a form can't submit twice, announces slow actions to screen readers, can stay focusable while disabled, and keeps the same behavior when it renders a link. For a plain click handler, a native `button` is enough.
6
+
7
+ ```tsx title="basic.tsx"
8
+ import { Button } from "@ui-sdk/react/button"
9
+ import { useLog } from "./log"
10
+
11
+ function Example() {
12
+ const log = useLog()
13
+
14
+ return <Button onClick={() => log("Pressed")}>Button</Button>
15
+ }
16
+
17
+ export { Example }
18
+ ```
19
+
20
+ Click Button, or focus it and press Enter or Space. The console logs each press.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @ui-sdk/react @ui-sdk/core
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Use `onClick` to run an action. Button renders a native `<button>` with `type="button"` by default.
31
+
32
+ ```tsx
33
+ import { Button } from "@ui-sdk/react/button"
34
+
35
+ ;<Button onClick={() => save()}>Save</Button>
36
+ ```
37
+
38
+ ## Pending
39
+
40
+ Return a Promise from `onClick` to keep the button pending until it settles. The button keeps focus and ignores further presses while it waits.
41
+
42
+ ```tsx
43
+ <Button
44
+ onClick={() => new Promise((resolve) => setTimeout(resolve, 1500))}
45
+ pendingAnnouncement="Saving"
46
+ >
47
+ Save
48
+ </Button>
49
+ ```
50
+
51
+ After one second, it announces "Loading" to screen readers. Set `pendingAnnouncement` to change that message, as above, or use an empty string to silence it.
52
+
53
+ ```tsx title="pending.tsx"
54
+ "use client"
55
+
56
+ import { Button } from "@ui-sdk/react/button"
57
+
58
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
59
+
60
+ function Example() {
61
+ return (
62
+ <Button onClick={() => wait(1500)} pendingAnnouncement="Saving">
63
+ Save
64
+ </Button>
65
+ )
66
+ }
67
+
68
+ export { Example }
69
+ ```
70
+
71
+ Press Save, then press it again while it saves. Nothing happens until the first action finishes.
72
+
73
+ ## Pending Content
74
+
75
+ To change what the button shows while it waits, mark content with `data-show="idle"` or `data-show="pending"` and hide it in the other state with one CSS rule each, or use Tailwind's `in-data-pending` variant:
76
+
77
+ ```css
78
+ [data-slot="button"]:not([data-pending]) [data-show="pending"],
79
+ [data-slot="button"][data-pending] [data-show="idle"] {
80
+ display: none;
81
+ }
82
+ ```
83
+
84
+ ```tsx title="pending-content.tsx"
85
+ "use client"
86
+
87
+ import { Button } from "@ui-sdk/react/button"
88
+ import { ArrowDownToLineIcon, LoaderCircleIcon } from "lucide-react"
89
+
90
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
91
+
92
+ function Example() {
93
+ return (
94
+ <Button onClick={() => wait(1500)} pendingAnnouncement="Saving">
95
+ <ArrowDownToLineIcon data-show="idle" aria-hidden="true" />
96
+ <LoaderCircleIcon data-show="pending" data-slot="spinner" aria-hidden="true" />
97
+ Save
98
+ </Button>
99
+ )
100
+ }
101
+
102
+ export { Example }
103
+ ```
104
+
105
+ Press Save. Its arrow turns into a spinner while it saves.
106
+
107
+ Keep the label and change only the visual, as above. If the text has to change too, swap it the same way, without an `aria-label`. [Pending Buttons](#pending-buttons) explains why:
108
+
109
+ ```tsx
110
+ <Button onClick={save}>
111
+ <span data-show="idle">Save</span>
112
+ <span data-show="pending">Saving…</span>
113
+ </Button>
114
+ ```
115
+
116
+ ## Handling Errors
117
+
118
+ If the handler throws or its Promise rejects, `state.error` holds the error. Use it to show a message or a retry label. The next press clears the error and runs the handler again.
119
+
120
+ The demo uses a [render function](../parts-and-composition.md#read-the-state) to show the error beside the button.
121
+
122
+ ```tsx title="render.tsx"
123
+ "use client"
124
+
125
+ import { Button } from "@ui-sdk/react/button"
126
+ import { useLog } from "./log"
127
+
128
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
129
+
130
+ function Example() {
131
+ const log = useLog()
132
+
133
+ return (
134
+ <div data-slot="stack">
135
+ <Button
136
+ onClick={async () => {
137
+ await wait(1000)
138
+ log("Couldn't save. Check your connection and try again.")
139
+ throw new Error("Couldn't save")
140
+ }}
141
+ render={(props, state) => (
142
+ <button {...props}>
143
+ {state.pending
144
+ ? "Saving…"
145
+ : state.error
146
+ ? "Try Again"
147
+ : "Save Changes"}
148
+ </button>
149
+ )}
150
+ />
151
+ </div>
152
+ )
153
+ }
154
+
155
+ export { Example }
156
+ ```
157
+
158
+ Choose Save Changes. Saving always fails here: the console shows the error and the label changes to Try Again.
159
+
160
+ ## Changes After an Await
161
+
162
+ `onClick` receives the event and `{ source }`, which says how the button was pressed. If the handler changes another ui-sdk component after an `await`, wrap that change in `withSource` to keep the original source. Otherwise, the later change is reported as `"programmatic"`.
163
+
164
+ Here `save()` is your request and `status` is an Input's [controller](../controllers.md):
165
+
166
+ ```tsx
167
+ "use client"
168
+
169
+ import { Button } from "@ui-sdk/react/button"
170
+ import { withSource } from "@ui-sdk/react/observe"
171
+
172
+ function SaveButton() {
173
+ return (
174
+ <Button
175
+ onClick={async (event, { source }) => {
176
+ await save()
177
+ withSource(source, () => status.setValue("Saved"))
178
+ }}
179
+ >
180
+ Save
181
+ </Button>
182
+ )
183
+ }
184
+
185
+ export { SaveButton }
186
+ ```
187
+
188
+ [Observe](../core.md#keep-the-source-after-an-await) explains source tracking. Preserving `"agent"` lets [`undoSource("agent")`](../agents.md#undo-the-agents-changes) include that later change.
189
+
190
+ ## Disabled
191
+
192
+ Set `disabled` to prevent presses and remove the button from the tab order. A disabled `<fieldset>` around the button disables it too.
193
+
194
+ ```tsx
195
+ <Button disabled>Disabled</Button>
196
+ ```
197
+
198
+ ```tsx title="disabled.tsx"
199
+ import { Button } from "@ui-sdk/react/button"
200
+
201
+ function Example() {
202
+ return <Button disabled>Disabled</Button>
203
+ }
204
+
205
+ export { Example }
206
+ ```
207
+
208
+ Try clicking the button. It does nothing, and Tab skips it.
209
+
210
+ ## Focusable When Disabled
211
+
212
+ Add `focusableWhenDisabled` when people should still be able to focus a disabled button. It uses `aria-disabled` instead of the native `disabled` attribute. Presses still do nothing.
213
+
214
+ Use `aria-describedby` to explain why the action is unavailable:
215
+
216
+ ```tsx
217
+ <Button disabled focusableWhenDisabled aria-describedby="publish-hint">
218
+ Publish
219
+ </Button>
220
+ <p id="publish-hint">Add a title to publish.</p>
221
+ ```
222
+
223
+ ```tsx title="focusable-when-disabled.tsx"
224
+ "use client"
225
+
226
+ import * as React from "react"
227
+ import { Button } from "@ui-sdk/react/button"
228
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
229
+ import { Input } from "@ui-sdk/react/input"
230
+ import { useLog } from "./log"
231
+
232
+ function Example() {
233
+ const log = useLog()
234
+ const [title, setTitle] = React.useState("")
235
+ const hint = React.useId()
236
+ const ready = title.trim() !== ""
237
+
238
+ return (
239
+ <div data-slot="stack">
240
+ <Field>
241
+ <FieldLabel>Title</FieldLabel>
242
+ <Input value={title} onValueChange={(value) => setTitle(value)} />
243
+ </Field>
244
+ <div data-slot="row">
245
+ <Button
246
+ disabled={!ready}
247
+ focusableWhenDisabled
248
+ aria-describedby={hint}
249
+ onClick={() => log(`Published “${title}”`)}
250
+ >
251
+ Publish
252
+ </Button>
253
+ <p id={hint} data-slot="hint">
254
+ {ready ? "Ready to publish." : "Add a title to publish."}
255
+ </p>
256
+ </div>
257
+ </div>
258
+ )
259
+ }
260
+
261
+ export { Example }
262
+ ```
263
+
264
+ Focus Title, then press Tab to reach Publish while it is disabled. Enter a title to enable it, then choose Publish. The console logs what was published.
265
+
266
+ ## Links
267
+
268
+ Pass an `<a>` to `render` to make a link. It keeps its `href` and link semantics, including opening in a new tab. A disabled link cannot navigate and leaves the tab order.
269
+
270
+ ```tsx
271
+ <Button render={<a href="/docs/react" />}>Get started</Button>
272
+ ```
273
+
274
+ ```tsx title="link.tsx"
275
+ import { Button } from "@ui-sdk/react/button"
276
+
277
+ function Example() {
278
+ return (
279
+ <div data-slot="row">
280
+ <Button render={<a href="#installation" />}>Get Started</Button>
281
+ <Button disabled render={<a href="#changelog" />}>
282
+ Changelog
283
+ </Button>
284
+ </div>
285
+ )
286
+ }
287
+
288
+ export { Example }
289
+ ```
290
+
291
+ Choose Get Started to jump to Installation on this page. Changelog is disabled and does not navigate.
292
+
293
+ For your own components, see [Parts and Composition](../parts-and-composition.md).
294
+
295
+ ## Forms
296
+
297
+ Set `type="submit"` to submit a form or `type="reset"` to restore its default values. The default, `type="button"`, does neither.
298
+
299
+ ```tsx
300
+ <Button type="submit">Subscribe</Button>
301
+ <Button type="reset">Reset</Button>
302
+ ```
303
+
304
+ ```tsx title="form.tsx"
305
+ "use client"
306
+
307
+ import { Field, FieldLabel } from "@ui-sdk/react/field"
308
+ import { Input } from "@ui-sdk/react/input"
309
+ import { Button } from "@ui-sdk/react/button"
310
+ import { useLog } from "./log"
311
+
312
+ function Example() {
313
+ const log = useLog()
314
+
315
+ return (
316
+ <form
317
+ data-slot="stack"
318
+ onSubmit={(event) => {
319
+ event.preventDefault()
320
+ const email = String(new FormData(event.currentTarget).get("email"))
321
+ log(`Subscribed ${email}`)
322
+ }}
323
+ >
324
+ <Field>
325
+ <FieldLabel>Email</FieldLabel>
326
+ <Input name="email" type="email" defaultValue="ada@example.com" />
327
+ </Field>
328
+ <div data-slot="row">
329
+ <Button type="submit">Subscribe</Button>
330
+ <Button type="reset">Reset</Button>
331
+ </div>
332
+ </form>
333
+ )
334
+ }
335
+
336
+ export { Example }
337
+ ```
338
+
339
+ Edit the email and choose Subscribe, or press Enter in the field. The console logs each submission, and Reset restores the original email.
340
+
341
+ Button tracks the Promise returned by its own `onClick`, not one from the form’s submit handler. For an async submit handler, keep your own pending flag and set `disabled` on the button while it runs, with [`focusableWhenDisabled`](#focusable-when-disabled) so the button keeps focus while it waits.
342
+
343
+ ## Styling
344
+
345
+ Use `data-pending` to show a spinner or waiting cursor, and `data-disabled` to style an unavailable action. The button's `data-slot` is `button`, and its attributes are listed under [Data Attributes](#data-attributes).
346
+
347
+ [Styling and Animation](../styling-and-animation.md) covers the styling patterns shared by all components.
348
+
349
+ ## Data Attributes
350
+
351
+ ### Button
352
+ `[data-slot="button"]`
353
+
354
+ - `data-pending`: Present while the Promise its press handler returned is running.
355
+ - `data-disabled`: Present when disabled.
356
+
357
+ ## API Reference
358
+
359
+ Button accepts native button props and the options below. `render`, `children`, `className` and `style` can read the button's state; see [Parts and Composition](../parts-and-composition.md).
360
+
361
+ ### Button
362
+
363
+ - `disabled?` (`boolean`): Stops presses. The button is natively disabled, so it leaves the tab order and its form doesn't submit through it, unless `focusableWhenDisabled` is set.
364
+ - `focusableWhenDisabled?` (`boolean`): Keeps a disabled button in the tab order with `aria-disabled` instead of the native attribute, so it can hold focus while it becomes disabled and host a tooltip that says why. Presses still do nothing.
365
+ - `type?` (`"button" | "reset" | "submit"`): What a native button does in a form: `"submit"` submits it and `"reset"` resets it. Ignored when the button renders as something else.
366
+ - `pendingAnnouncement?` (`string`): What screen readers hear once the button has been pending for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
367
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
368
+
369
+ ### State
370
+
371
+ - `disabled` (`boolean`): Whether it's disabled, by the `disabled` option or a disabled fieldset around it. A running press shows in `pending` instead.
372
+ - `pending` (`boolean`): Whether the Promise the last press returned is still running. Presses do nothing meanwhile.
373
+ - `error` (`unknown`): Why the last press failed: what its handler threw, or its Promise rejected with. Cleared by the next press.
374
+
375
+ ### Events
376
+
377
+ | Prop | Receives |
378
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
379
+ | `onClick` | React's click event and `{ source }`, how the button was pressed. Return a Promise to keep the button pending until it settles. |
380
+
381
+ ## Accessibility
382
+
383
+ Give the button a clear text label. If it contains only an icon, add an `aria-label`. Native buttons respond to Enter and Space; links respond to Enter.
384
+
385
+ ### Pending Buttons
386
+
387
+ While its action runs, the button tells assistive technology it's busy, so you don't need to add anything:
388
+
389
+ - It sets `aria-busy="true"` and `aria-disabled="true"`, and ignores presses until the action settles. It keeps focus the whole time.
390
+ - After a second, it announces `pendingAnnouncement` ("Loading" by default) through a polite live region. Quick actions stay silent. This is a status message: people hear it without focus moving.
391
+
392
+ To show progress, keep the label and change only the visual, like a spinner. Changing the text renames the button while it has focus: screen readers don't reliably announce a new name on the focused element, so the change mostly helps sighted users, and the button's width shifts as the text changes length. The announcement already tells everyone it's working.
393
+
394
+ ```tsx title="Do"
395
+ <Button onClick={save} pendingAnnouncement="Saving">
396
+ <ArrowDownToLineIcon data-show="idle" aria-hidden="true" />
397
+ <LoaderCircleIcon data-show="pending" aria-hidden="true" />
398
+ Save
399
+ </Button>
400
+ ```
401
+
402
+ ```tsx title="Don't"
403
+ <Button onClick={save} aria-label="Save">
404
+ {saving ? "Saving…" : "Save"}
405
+ <span aria-live="polite">{saving ? "Saving" : ""}</span>
406
+ </Button>
407
+ ```
408
+
409
+ The second button tracks `saving` itself, shows text its accessible name doesn't contain, and announces the same state twice. If the text has to change anyway, swap it with `data-show` and leave out `aria-label`, so the button's name always contains the text on screen. See [Pending Content](#pending-content) for the markup.
410
+
411
+ References:
412
+
413
+ - [WCAG 2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html)
414
+ - [WCAG 4.1.3 Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html)
415
+ - [WAI-ARIA `aria-busy`](https://www.w3.org/TR/wai-aria-1.2/#aria-busy) and [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled)
416
+
417
+ ### Tested Behavior
418
+
419
+ The following behaviors are covered by automated tests:
420
+
421
+ - Clicking a button runs its handler.
422
+ - Space presses a focused button.
423
+ - Enter presses a focused button.
424
+ - A submit button submits its form.
425
+ - A reset button resets its form.
426
+ - A disabled button can't be focused or pressed.
427
+ - A button rendered as a link stays a link.
428
+
429
+ **Focusable when disabled**
430
+
431
+ - It stays focusable and says it's disabled.
432
+ - Tab reaches it.
433
+ - Clicking it runs nothing and submits nothing.
434
+ - Space on it runs nothing and submits nothing.
435
+ - Enter on it runs nothing and submits nothing.
436
+
437
+ **Pending**
438
+
439
+ - A returned Promise makes it busy, and it keeps focus until the Promise settles.
440
+ - While pending, it says it's unavailable with aria-disabled and keeps focus.
441
+ - Still pending after a second, it tells screen readers it's loading.
442
+ - A rejected Promise ends the pending state.
443
+
444
+ Runs in React, Vue and web components.