@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
package/docs/llms.txt ADDED
@@ -0,0 +1,1844 @@
1
+ # ui-sdk for React
2
+
3
+ > @ui-sdk/react renders ui-sdk's framework-free TypeScript core in React. How an interface behaves lives in the core; @ui-sdk/react turns each primitive into parts. No styles, no dependencies, no providers, no portals.
4
+
5
+ This file is the context an agent needs to build with @ui-sdk/react: how React renders a primitive and the conventions every part shares, then every component with a link to its page. Read the pages of the components you use. It's generated from the docs, for this release.
6
+
7
+ - [Core](./core.md): build a component, compose behaviors, and explore the foundations, from defining a primitive to working with state, the browser and text.
8
+ - [Agents](./agents.md): letting an agent operate a page, and WebMCP.
9
+
10
+ ## Introduction
11
+
12
+ These docs cover ui-sdk in React. Using another framework? See Vue, Svelte, Solid or Web Components. To build your own components from behaviors, see [Core](./core.md).
13
+
14
+ `@ui-sdk/react` gives you components such as Select, Dialog, and Tabs, built on ui-sdk's behaviors. They handle interactions, focus, and ARIA attributes. You choose their markup and styles.
15
+
16
+ Import the parts you need, put them together in your app, and style them with your own CSS.
17
+
18
+ ### Quick Start
19
+
20
+ Copy this prompt into your agent:
21
+
22
+ ```text prompt
23
+ Set up ui-sdk in this React project. It needs React 19 or newer.
24
+
25
+ 1. Install @ui-sdk/react and @ui-sdk/core with the project's package manager.
26
+ 2. Add a note to AGENTS.md, creating it if needed: before using ui-sdk, follow the ui-sdk skill if it's installed, and read node_modules/@ui-sdk/react/docs/llms.txt. To build a new component, also search node_modules/@ui-sdk/react/docs/core.md by heading, and read rendering.md next to it.
27
+ 3. If you support skills, run: npx skills add vercel-labs/ui --skill ui-sdk
28
+ ```
29
+
30
+ It installs ui-sdk and points your `AGENTS.md` at the docs that ship with the packages.
31
+
32
+ ### Set It Up Yourself
33
+
34
+ - [Installation](./installation.md): Install the packages in your React app.
35
+ - [First Component](./first-component.md): Add and style your first component.
36
+
37
+ ### Explore the Components
38
+
39
+ The [Using Components guides](./parts-and-composition.md) cover patterns the components share, such as composing parts, handling state, and styling.
40
+
41
+ ## Installation
42
+
43
+ Start with an app using React 19 and React DOM 19 or newer.
44
+
45
+ ### Install the Packages
46
+
47
+ ```bash
48
+ npm install @ui-sdk/react @ui-sdk/core
49
+ ```
50
+
51
+ `@ui-sdk/react` provides the React components. `@ui-sdk/core` provides their behavior and is a peer dependency. Install both so the components and any helpers you use share one copy of the core.
52
+
53
+ If you use TypeScript, you'll also need `@types/react` 19.2.10 or newer.
54
+
55
+ ### Import a Component
56
+
57
+ Import each component from its own entry:
58
+
59
+ ```tsx
60
+ import { Button } from "@ui-sdk/react/button"
61
+ import { Toggle } from "@ui-sdk/react/toggle"
62
+ ```
63
+
64
+ A Dialog has separate parts for its trigger, content, and title. Import those parts from the same entry:
65
+
66
+ ```tsx
67
+ import {
68
+ Dialog,
69
+ DialogContent,
70
+ DialogTitle,
71
+ DialogTrigger,
72
+ } from "@ui-sdk/react/dialog"
73
+ ```
74
+
75
+ There is no root import from `@ui-sdk/react`. Each [component page](./README.md) lists the parts you can use.
76
+
77
+ ### Add Your Styles
78
+
79
+ Components are unstyled, so you style them with your app's CSS or with Tailwind CSS. [Styling and Animation](./styling-and-animation.md) covers both.
80
+
81
+ You can render your first component without setting up a provider or importing a UI SDK stylesheet. If you use Next.js, [Server Rendering](./server-rendering.md) explains when your own components need `"use client"`.
82
+
83
+ Continue with [First Component](./first-component.md) to add a working toggle to your app.
84
+
85
+ ## First Component
86
+
87
+ Let's add a Bookmark toggle. Press it once to turn it on, then again to turn it off.
88
+
89
+ If you haven't installed the packages yet, start with [Installation](./installation.md).
90
+
91
+ ### Create the Component
92
+
93
+ Create `bookmark-button.tsx` next to your app component and render a `Toggle`:
94
+
95
+ ```tsx title="bookmark-button.tsx"
96
+ import { Toggle } from "@ui-sdk/react/toggle"
97
+
98
+ function BookmarkButton() {
99
+ return <Toggle>Bookmark</Toggle>
100
+ }
101
+
102
+ export { BookmarkButton }
103
+ ```
104
+
105
+ `Toggle` renders a button. It keeps track of its pressed state, handles Enter and Space, and sets `aria-pressed` for screen readers.
106
+
107
+ ### Use It in Your App
108
+
109
+ In your app component (`app.tsx` here), import and render `BookmarkButton`:
110
+
111
+ ```tsx title="app.tsx"
112
+ import { BookmarkButton } from "./bookmark-button"
113
+
114
+ function App() {
115
+ return <BookmarkButton />
116
+ }
117
+
118
+ export { App }
119
+ ```
120
+
121
+ Open your app. You should see a button labeled Bookmark. Next, add styles so you can tell when it is pressed.
122
+
123
+ ### Add Styles
124
+
125
+ When the toggle is pressed, it has a `data-pressed` attribute. Use that attribute to change its appearance.
126
+
127
+ With Tailwind CSS configured in your app, add utility classes to `className`. The `data-pressed:` variant applies while the toggle is pressed:
128
+
129
+ ```tsx title="bookmark-button.tsx"
130
+ import { Toggle } from "@ui-sdk/react/toggle"
131
+
132
+ function BookmarkButton() {
133
+ return (
134
+ <Toggle className="inline-flex h-8 items-center rounded-md border border-neutral-300 px-3 text-sm font-medium hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-offset-2 data-pressed:bg-neutral-900 data-pressed:text-white">
135
+ Bookmark
136
+ </Toggle>
137
+ )
138
+ }
139
+
140
+ export { BookmarkButton }
141
+ ```
142
+
143
+ Try the styled toggle:
144
+
145
+ ```tsx title="first-component.tsx"
146
+ import { Toggle } from "@ui-sdk/react/toggle"
147
+
148
+ function Example() {
149
+ return <Toggle data-variant="outline">Bookmark</Toggle>
150
+ }
151
+
152
+ export { Example }
153
+ ```
154
+
155
+ Click Bookmark to turn it on or off. A filled background means it is on. You can also focus the button with Tab and press Enter or Space.
156
+
157
+ Keep the label the same in both states. `aria-pressed` tells a screen reader whether the toggle is on.
158
+
159
+ ### Keep Going
160
+
161
+ - [Toggle](./components/toggle.md) covers disabled states, controlled state, and groups of toggles.
162
+ - [Components](./README.md) lists the other components you can add with the same install.
163
+ - [Parts and Composition](./parts-and-composition.md) shows how to put a component with several parts together.
164
+ - [State and Events](./state-and-events.md) shows how to read changes and control state from your app.
165
+ - [Styling and Animation](./styling-and-animation.md) covers state attributes, CSS variables, and transitions.
166
+
167
+ ## Parts and Composition
168
+
169
+ A component can have several parts. A Popover has a root to hold its state, a trigger to open it, and content to show.
170
+
171
+ ### Put Parts Together
172
+
173
+ Import the parts you need and nest them as shown in the component's Composition:
174
+
175
+ ```tsx
176
+ import {
177
+ Popover,
178
+ PopoverClose,
179
+ PopoverContent,
180
+ PopoverTitle,
181
+ PopoverTrigger,
182
+ } from "@ui-sdk/react/popover"
183
+
184
+ function Example() {
185
+ return (
186
+ <Popover>
187
+ <PopoverTrigger>Details</PopoverTrigger>
188
+ <PopoverContent sideOffset={8}>
189
+ <PopoverTitle>Project Details</PopoverTitle>
190
+ <p>Last updated today.</p>
191
+ <PopoverClose>Done</PopoverClose>
192
+ </PopoverContent>
193
+ </Popover>
194
+ )
195
+ }
196
+
197
+ export { Example }
198
+ ```
199
+
200
+ `Popover` shares state with its parts without adding an element. Other roots, such as `Tabs`, do render an element. Each component's page shows its structure.
201
+
202
+ Parts find their root through React context. You can put your own components or layout elements between them.
203
+
204
+ ### Choose the Element
205
+
206
+ Use `render` to replace a part's default element. For example, render a menu item as a link:
207
+
208
+ ```tsx
209
+ import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/react/menu"
210
+
211
+ function Example() {
212
+ return (
213
+ <Menu>
214
+ <MenuTrigger>Account</MenuTrigger>
215
+ <MenuContent>
216
+ <MenuItem render={<a href="/settings" />}>Settings</MenuItem>
217
+ </MenuContent>
218
+ </Menu>
219
+ )
220
+ }
221
+
222
+ export { Example }
223
+ ```
224
+
225
+ The part passes its attributes, handlers, and ref to the element. A custom component used here needs to forward those props and the ref to its DOM element.
226
+
227
+ Your event handler runs first. Calling `event.preventDefault()` skips the part's built-in handler. Class names, styles, and refs combine.
228
+
229
+ ### Read the State
230
+
231
+ Pass a function to `children` when the content depends on state:
232
+
233
+ ```tsx
234
+ "use client"
235
+
236
+ import { Toggle } from "@ui-sdk/react/toggle"
237
+
238
+ function Example() {
239
+ return (
240
+ <Toggle aria-label="Mute">
241
+ {(state) => (state.pressed ? "Mute on" : "Mute off")}
242
+ </Toggle>
243
+ )
244
+ }
245
+
246
+ export { Example }
247
+ ```
248
+
249
+ Keep the accessible name stable when the content changes. Here, `aria-label="Mute"` names the toggle and `aria-pressed` reports its state.
250
+
251
+ `render` can also be a function of the props and state. [Custom Parts](./custom-parts.md) covers writing a part with logic of its own.
252
+
253
+ ### Name the Control
254
+
255
+ Use the component's label or title part, such as `FieldLabel` or `DialogTitle`. For a control without visible text, give it an `aria-label`.
256
+
257
+ Parts manage their own roles, IDs, and ARIA state. Those attributes take precedence over conflicting props passed directly to the part. Props on the element passed to `render` can override them, so leave the managed attributes intact. If you use a render function, forward the supplied props to its element. A name or typing hint you pass replaces the part's own: `aria-label`, `aria-labelledby`, `aria-description`, `title`, `autoComplete`, `autoCapitalize`, `autoCorrect`, `inputMode` and `spellCheck`. Roles, `tabindex`, references like `aria-controls` and `aria-describedby`, state attributes and `data-slot` stay the part's, since its behavior reads them.
258
+
259
+ ## State and Events
260
+
261
+ Components manage their own state unless you pass a controlled value. A change callback lets you respond in either case.
262
+
263
+ ### Set the Starting Value
264
+
265
+ Use a default prop to choose the starting state. Here, Bold starts pressed and changes when you click it:
266
+
267
+ ```tsx
268
+ import { Toggle } from "@ui-sdk/react/toggle"
269
+
270
+ function Example() {
271
+ return <Toggle defaultPressed>Bold</Toggle>
272
+ }
273
+
274
+ export { Example }
275
+ ```
276
+
277
+ The prop depends on the component: `defaultPressed`, `defaultChecked`, `defaultOpen`, or `defaultValue`.
278
+
279
+ ### Control the Value
280
+
281
+ Pass the value and its change callback to keep it in React state:
282
+
283
+ ```tsx
284
+ "use client"
285
+
286
+ import { Toggle } from "@ui-sdk/react/toggle"
287
+ import { useState } from "react"
288
+
289
+ function Example() {
290
+ const [pressed, setPressed] = useState(false)
291
+
292
+ return (
293
+ <>
294
+ <Toggle pressed={pressed} onPressedChange={setPressed}>
295
+ Bold
296
+ </Toggle>
297
+ <output>{pressed ? "On" : "Off"}</output>
298
+ </>
299
+ )
300
+ }
301
+
302
+ export { Example }
303
+ ```
304
+
305
+ Other components use pairs such as `open` and `onOpenChange`, or `value` and `onValueChange`. The component requests the next value; your callback updates the state you pass back. If you keep passing `false`, the toggle stays off.
306
+
307
+ To keep a value in the URL, still update your state in the callback, then navigate. A value that changes only when the navigation finishes leaves the component showing the old one until then, and logs a warning in development.
308
+
309
+ ### Understand a Change
310
+
311
+ The first callback argument is the requested value. The second, `details`, tells you where the request came from and lets you cancel it:
312
+
313
+ | Detail | What It Tells You |
314
+ | ---------- | ----------------------------------------------------------------------------------------------------- |
315
+ | `reason` | What requested the change, such as `trigger-press` or `escape-key`. Each component lists its reasons. |
316
+ | `event` | The DOM event, when the change came from one. |
317
+ | `source` | `keyboard`, `mouse`, `touch`, `pen`, `virtual`, `programmatic`, or `agent`. |
318
+ | `cancel()` | Stops the requested change. |
319
+
320
+ For example, keep a popover open when someone clicks outside it:
321
+
322
+ ```tsx
323
+ "use client"
324
+
325
+ import {
326
+ Popover,
327
+ PopoverClose,
328
+ PopoverContent,
329
+ PopoverTitle,
330
+ PopoverTrigger,
331
+ } from "@ui-sdk/react/popover"
332
+
333
+ function Example() {
334
+ return (
335
+ <Popover
336
+ onOpenChange={(open, details) => {
337
+ if (!open && details.reason === "outside-press") {
338
+ details.cancel()
339
+ }
340
+ }}
341
+ >
342
+ <PopoverTrigger>Details</PopoverTrigger>
343
+ <PopoverContent>
344
+ <PopoverTitle>Project Details</PopoverTitle>
345
+ <p>Last updated today.</p>
346
+ <PopoverClose>Done</PopoverClose>
347
+ </PopoverContent>
348
+ </Popover>
349
+ )
350
+ }
351
+
352
+ export { Example }
353
+ ```
354
+
355
+ The Done button and Escape still close the popover. Cancellation works for controlled and uncontrolled state. Add `debug` to a root to log changes and their reasons while you work.
356
+
357
+ ### Empty and Multiple Values
358
+
359
+ Select, Tabs, and Radio Group report `null` when nothing is selected. A multiple Select or Combobox uses an array. Accordion and Toggle Group always use arrays.
360
+
361
+ To change a component from outside its React tree, use a [Controller](./controllers.md).
362
+
363
+ ## Styling and Animation
364
+
365
+ Parts render without styles. The component demos use plain CSS, and the same state attributes and CSS variables work with Tailwind CSS or any other stylesheet.
366
+
367
+ ### Style a Part
368
+
369
+ Every part has a `data-slot` attribute. State adds attributes such as `data-open`, `data-pressed`, or `data-disabled`:
370
+
371
+ ```tsx
372
+ import { Toggle } from "@ui-sdk/react/toggle"
373
+
374
+ function Example() {
375
+ return (
376
+ <Toggle className="rounded-md border border-neutral-300 px-2.5 py-1.5 text-sm data-pressed:bg-neutral-100 data-disabled:opacity-50">
377
+ Bold
378
+ </Toggle>
379
+ )
380
+ }
381
+
382
+ export { Example }
383
+ ```
384
+
385
+ Most state attributes are present or absent. Others have values, such as `data-side="top"` or `data-orientation="vertical"`. Each component's Styling section lists its attributes and CSS variables. Use these rather than generated IDs, which can change.
386
+
387
+ ### CSS Modules
388
+
389
+ Import a CSS Module and pass its class to the part. State selectors work the same way:
390
+
391
+ ```tsx
392
+ import { Toggle } from "@ui-sdk/react/toggle"
393
+ import styles from "./toggle.module.css"
394
+
395
+ function Example() {
396
+ return <Toggle className={styles.toggle}>Bold</Toggle>
397
+ }
398
+
399
+ export { Example }
400
+ ```
401
+
402
+ ```css title="toggle.module.css"
403
+ .toggle {
404
+ cursor: default;
405
+ border: 1px solid var(--input, #e5e5e5);
406
+ border-radius: 1.125rem;
407
+ background: transparent;
408
+ padding: 0.375rem 0.625rem;
409
+ color: inherit;
410
+ font: inherit;
411
+ font-size: 0.875rem;
412
+ }
413
+
414
+ .toggle[data-pressed] {
415
+ background: var(--muted, #f5f5f5);
416
+ }
417
+
418
+ .toggle[data-disabled] {
419
+ opacity: 0.5;
420
+ }
421
+
422
+ .toggle:focus-visible {
423
+ outline: 2px solid var(--ring, #a3a3a3);
424
+ outline-offset: 2px;
425
+ }
426
+ ```
427
+
428
+ ### Use State in React
429
+
430
+ `className` and `style` can be functions of the part's state:
431
+
432
+ ```tsx
433
+ "use client"
434
+
435
+ import { Toggle } from "@ui-sdk/react/toggle"
436
+
437
+ function Example() {
438
+ return (
439
+ <Toggle
440
+ className={(state) => (state.pressed ? "font-bold" : "font-normal")}
441
+ >
442
+ Bold
443
+ </Toggle>
444
+ )
445
+ }
446
+
447
+ export { Example }
448
+ ```
449
+
450
+ With Tailwind, you can also use attribute variants, such as `data-pressed:font-bold`.
451
+
452
+ ### Animate Opening and Closing
453
+
454
+ Popup content gets `data-starting-style` before it enters and `data-ending-style` while it exits. The content stays mounted until its exit transition or animation finishes.
455
+
456
+ ```tsx
457
+ import { Popover, PopoverContent, PopoverTrigger } from "@ui-sdk/react/popover"
458
+
459
+ function Example() {
460
+ return (
461
+ <Popover>
462
+ <PopoverTrigger>Details</PopoverTrigger>
463
+ <PopoverContent
464
+ aria-label="Project details"
465
+ className="origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-96 data-starting-style:opacity-0 data-ending-style:scale-96 data-ending-style:opacity-0 motion-reduce:transition-none"
466
+ >
467
+ Last updated today.
468
+ </PopoverContent>
469
+ </Popover>
470
+ )
471
+ }
472
+
473
+ export { Example }
474
+ ```
475
+
476
+ Put exit styles on `data-ending-style`. `data-closed` describes the state, but doesn't keep the element around for an exit.
477
+
478
+ Popups also provide `--anchor-width` and `--available-height` for sizing. If an animation library needs closed content to stay mounted, use `keepMounted`. See [Popups](./popups.md) for placement and mounting options.
479
+
480
+ ### The ui-sdk Layer
481
+
482
+ A few behaviors need a little CSS, like keeping a closed popup hidden or dimming the page behind a sheet, so ui-sdk adds it to the page in a cascade layer named `ui-sdk`. Declare that layer at the top of your global stylesheet, the one your app's entry or root layout imports, so your own styles win over it wherever the page puts them. With Tailwind, put it above `@import "tailwindcss"`:
483
+
484
+ ```css
485
+ @layer ui-sdk;
486
+ ```
487
+
488
+ A CSS Module loads with its component, too late to come first, so keep the line in the global stylesheet.
489
+
490
+ ui-sdk adds this CSS once a component mounts, so HTML rendered on the server doesn't have it until the page hydrates. Closed content kept in the page, like a tooltip's, carries `hidden`, and a class that sets `display` shows it until then. Tailwind's base styles already hide `hidden` elements. With other CSS, add the same rule to your global stylesheet:
491
+
492
+ ```css
493
+ @layer ui-sdk {
494
+ :where([data-slot][hidden]:not([hidden="until-found"])) {
495
+ display: none !important;
496
+ }
497
+ }
498
+ ```
499
+
500
+ It leaves `hidden="until-found"` alone, so find in page can still open a closed panel.
501
+
502
+ ### Content Security Policy
503
+
504
+ A Content Security Policy without `'unsafe-inline'` in `style-src` blocks the CSS ui-sdk adds. Give ui-sdk your policy's nonce with a meta tag in the page's `<head>`:
505
+
506
+ ```html
507
+ <meta property="csp-nonce" nonce="your-nonce" />
508
+ ```
509
+
510
+ With Vite, set `html.cspNonce` in your Vite config to a placeholder, and Vite writes this tag for you. Your server replaces the placeholder with a new nonce on each request.
511
+
512
+ In Next.js, render the tag in the root layout's `<head>`, with the nonce your middleware passes in a request header:
513
+
514
+ ```tsx
515
+ import { headers } from "next/headers"
516
+
517
+ export default async function RootLayout({
518
+ children,
519
+ }: {
520
+ children: React.ReactNode
521
+ }) {
522
+ const nonce = (await headers()).get("x-nonce") ?? undefined
523
+ return (
524
+ <html lang="en">
525
+ <head>
526
+ <meta property="csp-nonce" nonce={nonce} />
527
+ </head>
528
+ <body>{children}</body>
529
+ </html>
530
+ )
531
+ }
532
+ ```
533
+
534
+ Pages rendered on the server also carry a few `style` attributes, like the one that hides a select's native input, so the policy needs `style-src-attr 'unsafe-inline'` too.
535
+
536
+ If your policy has no nonce for styles, add the tag without the `nonce` attribute. ui-sdk then adds its CSS as constructed style sheets, which no policy blocks. If your code replaces `document.adoptedStyleSheets`, ui-sdk adds its sheets back the next time a component mounts.
537
+
538
+ ## Controllers
539
+
540
+ A controller connects code to a mounted component. Use one when the button or action that changes a component lives somewhere else.
541
+
542
+ ### Connect a Controller
543
+
544
+ Create the controller once and pass it to the root:
545
+
546
+ ```tsx
547
+ "use client"
548
+
549
+ import { createToggleController, Toggle } from "@ui-sdk/react/toggle"
550
+ import { useState } from "react"
551
+
552
+ function Example() {
553
+ const [toggle] = useState(() => createToggleController())
554
+
555
+ return (
556
+ <>
557
+ <button onClick={() => toggle.toggle()}>Toggle Bold</button>
558
+ <Toggle controller={toggle}>Bold</Toggle>
559
+ </>
560
+ )
561
+ }
562
+
563
+ export { Example }
564
+ ```
565
+
566
+ Controller methods go through the component's change callbacks. They follow the same controlled state and cancellation rules as user input.
567
+
568
+ Each component lists the methods it supports, such as `open()`, `close()`, `setValue()`, or `focus()`. Methods that change state accept an optional reason for the callback.
569
+
570
+ A read like `toggle.isPressed()` answers when you call it, so call it from an event handler or async code. It returns `undefined` until the component mounts, and a render doesn't update when the state changes. To render the state, keep it in state with `pressed` and `onPressedChange`.
571
+
572
+ ### Share a Popup
573
+
574
+ A trigger outside a popup can use its controller. Pass a `payload` to tell the content which item was opened:
575
+
576
+ ```tsx
577
+ "use client"
578
+
579
+ import {
580
+ createPopoverController,
581
+ Popover,
582
+ PopoverContent,
583
+ PopoverTitle,
584
+ PopoverTrigger,
585
+ } from "@ui-sdk/react/popover"
586
+ import { useState } from "react"
587
+
588
+ function Example() {
589
+ const [editor] = useState(() => createPopoverController<string>())
590
+
591
+ return (
592
+ <>
593
+ <PopoverTrigger controller={editor} payload="Notes">
594
+ Edit Notes
595
+ </PopoverTrigger>
596
+ <PopoverTrigger controller={editor} payload="Tasks">
597
+ Edit Tasks
598
+ </PopoverTrigger>
599
+ <Popover controller={editor}>
600
+ <PopoverContent>
601
+ {(state) => (
602
+ <PopoverTitle>Edit {String(state.payload ?? "")}</PopoverTitle>
603
+ )}
604
+ </PopoverContent>
605
+ </Popover>
606
+ </>
607
+ )
608
+ }
609
+
610
+ export { Example }
611
+ ```
612
+
613
+ Both triggers open the same popover. This is useful for a table with an edit button on each row.
614
+
615
+ ### Wait for a Popup to Close
616
+
617
+ A popup controller's `open()` returns a promise. It settles when the popup closes, with its reason and return value:
618
+
619
+ ```ts
620
+ const result = await editor.open({ payload: "Notes" })
621
+ console.log(result.reason, result.returnValue)
622
+ ```
623
+
624
+ For a confirmation dialog, [Alert Dialog](./components/alert-dialog.md) provides `createConfirm()`, which gives you a function you can await.
625
+
626
+ ## Popups
627
+
628
+ Popovers, menus, selects, and dialogs use the browser's top layer. They render in place, above the page, so you don't need a Portal or Positioner part. Their events bubble through the elements around them like any other child's, so a click handler or a link wrapped around a popup also gets the clicks inside it. Keep popups outside clickable cards and links.
629
+
630
+ ### Place the Content
631
+
632
+ Set placement on the content part:
633
+
634
+ ```tsx
635
+ import { Popover, PopoverContent, PopoverTrigger } from "@ui-sdk/react/popover"
636
+
637
+ function Example() {
638
+ return (
639
+ <Popover>
640
+ <PopoverTrigger>Details</PopoverTrigger>
641
+ <PopoverContent side="bottom" align="start" sideOffset={8}>
642
+ Last updated today.
643
+ </PopoverContent>
644
+ </Popover>
645
+ )
646
+ }
647
+
648
+ export { Example }
649
+ ```
650
+
651
+ The popup flips or shifts when there isn't enough room.
652
+
653
+ | Prop | What It Does |
654
+ | ------------------ | ---------------------------------------------------------------------------------------------------- |
655
+ | `side` | Chooses `top`, `bottom`, `left`, `right`, `start`, or `end`. Start and end follow reading direction. |
656
+ | `align` | Aligns the popup to the `start`, `center`, or `end` of that side. |
657
+ | `sideOffset` | Sets the gap from the anchor, in pixels. |
658
+ | `alignOffset` | Shifts the popup along its side, in pixels. |
659
+ | `collisionPadding` | Leaves space from the window's edges. |
660
+ | `arrowPadding` | Keeps an arrow away from the popup's corners. |
661
+
662
+ An `anchor` can position the popup against another element. The component's page lists the anchors it accepts.
663
+
664
+ ### Control Dismissal
665
+
666
+ Set `dismissible={false}` to stop Escape, outside presses, focus leaving, Back, and sheet swipes from closing a popup. Its trigger, Close part, and controller can still close it.
667
+
668
+ ```tsx
669
+ <Popover
670
+ dismissible={false}
671
+ onDismissAttempt={(details) => console.log(details.reason)}
672
+ >
673
+ {/* Trigger, content, and a Close part. */}
674
+ </Popover>
675
+ ```
676
+
677
+ Use [State and Events](./state-and-events.md) when you want to cancel only a particular reason, such as an outside press while a form has unsaved changes.
678
+
679
+ ### Modals and Sheets
680
+
681
+ `modal` makes the rest of the page inert and locks scrolling. Each component documents its default.
682
+
683
+ Menu, Select, Combobox, Autocomplete, Popover, and Dialog support `presentation="auto"` to become a sheet on small touch screens. Use `presentation="sheet"` to always show a sheet.
684
+
685
+ ```tsx
686
+ <Popover presentation="auto">{/* Trigger and content. */}</Popover>
687
+ ```
688
+
689
+ ### Focus
690
+
691
+ Dialogs, alert dialogs, drawers, and popovers move focus into their content when they open. Where it lands depends on how they opened:
692
+
693
+ | Opened With | Focus Goes To |
694
+ | -------------------------------------------- | ------------------------------------------------------------ |
695
+ | Keyboard, screen reader, mouse, pen, or code | The element `initialFocus` picks. |
696
+ | Touch | The content itself, so no on-screen keyboard appears. |
697
+ | Hover | Nowhere. Focus stays where it is. |
698
+ | Rendered open | Nowhere, except in a dialog, which takes focus as it mounts. |
699
+
700
+ `initialFocus` defaults to `"auto"`: the first focusable element, or the content when it has none. A dialog tries an element with `autofocus` first, and an alert dialog tries its first Close part. Pass `"content"` to focus the content itself, `"none"` to leave focus alone, or a CSS selector, an element, or a function that returns one.
701
+
702
+ `modal` decides whether Tab can leave. With `true` or `"trap-focus"`, Tab and Shift+Tab wrap inside the content. With `false`, Tab moves from the trigger into a popover and on to the page after it, and focus leaving closes the popover. A non-modal dialog stays open.
703
+
704
+ When a popup closes while focus is inside it, focus returns to the trigger. If the trigger is gone or can't take focus, focus goes to the element focused before the popup opened, then where the popup it was opened from would return focus, then to the nearest focusable element after where the trigger was. A popup a controller opened returns to the element focused before it first. Set `finalFocus` to `"none"` to leave focus alone, or to a selector, an element, or a function to send it somewhere else. Closing because focus moved away never moves it back, and neither does pressing something focusable outside a non-modal popup.
705
+
706
+ Menus, selects, and comboboxes handle focus within their own lists, and tooltips never move it. Each component's page covers its own.
707
+
708
+ ### Mounting and Containers
709
+
710
+ Closed content unmounts after its exit animation. `keepMounted` leaves it in the page, hidden. `onOpenChangeComplete` runs when opening or closing finishes, including animations.
711
+
712
+ Use `container` to render into another element, a shadow root, or an element in an iframe. Focus, Escape, and scroll locking then work in the document and shadow root the content renders in. Keep a dialog containing a form outside any surrounding form, or move its content with `container`, to avoid nested forms.
713
+
714
+ In a tree a hidden `<Activity>` or a Suspense fallback hides, an open popup hides with it, from its `container` too, and comes back open when the tree shows, with focus in it if it's modal.
715
+
716
+ [Styling and Animation](./styling-and-animation.md) shows how to animate the content.
717
+
718
+ ### Toasts and Confirms
719
+
720
+ Mount `OverlayHost` once near your app's root, then call `toast()` from event handlers:
721
+
722
+ ```tsx
723
+ "use client"
724
+
725
+ import { OverlayHost } from "@ui-sdk/react/overlays"
726
+ import { toast } from "@ui-sdk/react/toast"
727
+
728
+ function Example() {
729
+ return (
730
+ <>
731
+ <OverlayHost />
732
+ <button onClick={() => toast.success("Saved")}>Save</button>
733
+ </>
734
+ )
735
+ }
736
+
737
+ export { Example }
738
+ ```
739
+
740
+ See [Toast](./components/toast.md) and [Alert Dialog](./components/alert-dialog.md) for the complete APIs.
741
+
742
+ ## Lists and Menus
743
+
744
+ Select, Combobox, Autocomplete, Listbox, and Trigger Menu accept items as data. Use this when your choices already come from an array. You can also compose their item parts yourself.
745
+
746
+ ### Start with Strings
747
+
748
+ Give a Select an array and render its trigger and content:
749
+
750
+ ```tsx
751
+ import {
752
+ Select,
753
+ SelectContent,
754
+ SelectTrigger,
755
+ SelectValue,
756
+ } from "@ui-sdk/react/select"
757
+
758
+ function Example() {
759
+ return (
760
+ <Select items={["Apple", "Banana", "Pear"]}>
761
+ <SelectTrigger aria-label="Fruit">
762
+ <SelectValue placeholder="Choose a fruit" />
763
+ </SelectTrigger>
764
+ <SelectContent />
765
+ </Select>
766
+ )
767
+ }
768
+
769
+ export { Example }
770
+ ```
771
+
772
+ Each string is both the value and its label. Use objects when those need to differ:
773
+
774
+ ```tsx
775
+ <Select
776
+ items={[
777
+ { value: "apple", label: "Apple" },
778
+ { value: "banana", label: "Banana", disabled: true },
779
+ ]}
780
+ >
781
+ {/* Trigger and content. */}
782
+ </Select>
783
+ ```
784
+
785
+ Items can also have `keywords` for searching. Group them with `{ type: "group", label, items }`, or separate them with `{ type: "separator" }`. Type the array with `ListData`, so each entry is checked where you write it:
786
+
787
+ ```tsx
788
+ import type { ListData } from "@ui-sdk/react/select"
789
+
790
+ const fruits: ListData = [
791
+ { value: "apple", label: "Apple", keywords: ["red"] },
792
+ { type: "separator" },
793
+ { type: "group", label: "Citrus", items: ["Lemon", "Lime"] },
794
+ ]
795
+ ```
796
+
797
+ Command, Menu and Trigger Menu take the same entries, plus fields of their own like a command's `shortcut`. Type them with `CommandData`, `MenuData` and `TriggerMenuData`. Tree takes `TreeItemData[]`.
798
+
799
+ If the value itself is an object, provide `itemToValue` for its submitted string and `itemToLabel` for its displayed text. [Select](./components/select.md) and [Combobox](./components/combobox.md) show complete examples.
800
+
801
+ ### Style the Items
802
+
803
+ Items from data render as bare parts, with no classes, check marks or arrows. To style them, render the parts yourself. For an array you have, map it into items, with your classes inline:
804
+
805
+ ```tsx
806
+ import {
807
+ Select,
808
+ SelectContent,
809
+ SelectItem,
810
+ SelectItemIndicator,
811
+ SelectTrigger,
812
+ SelectValue,
813
+ } from "@ui-sdk/react/select"
814
+
815
+ const fruits = [
816
+ { value: "apple", label: "Apple" },
817
+ { value: "banana", label: "Banana" },
818
+ ]
819
+
820
+ function Example() {
821
+ return (
822
+ <Select items={fruits}>
823
+ <SelectTrigger aria-label="Fruit">
824
+ <SelectValue placeholder="Choose a fruit" />
825
+ </SelectTrigger>
826
+ <SelectContent className="rounded-2xl bg-white p-1">
827
+ {fruits.map((fruit) => (
828
+ <SelectItem
829
+ key={fruit.value}
830
+ value={fruit.value}
831
+ className="flex rounded-xl px-2 py-1.5 data-highlighted:bg-neutral-100"
832
+ >
833
+ {fruit.label}
834
+ <SelectItemIndicator className="ms-auto">✓</SelectItemIndicator>
835
+ </SelectItem>
836
+ ))}
837
+ </SelectContent>
838
+ </Select>
839
+ )
840
+ }
841
+
842
+ export { Example }
843
+ ```
844
+
845
+ Keeping `items` on the select lets it show the chosen label before the list first renders. Searchable components still filter the items you render: the ones that don't match are hidden.
846
+
847
+ Items that only exist in state, like results from a server, render from the list's children function, with `state.items`:
848
+
849
+ ```tsx
850
+ <ComboboxList>
851
+ {(state) =>
852
+ (state.items as { value: string; label: string }[]).map((item) => (
853
+ <ComboboxItem
854
+ key={item.value}
855
+ value={item.value}
856
+ className="rounded-xl px-2 py-1.5 data-highlighted:bg-neutral-100"
857
+ >
858
+ {item.label}
859
+ </ComboboxItem>
860
+ ))
861
+ }
862
+ </ComboboxList>
863
+ ```
864
+
865
+ ### Search and Load Items
866
+
867
+ Searchable components accept a `filter` to change how typing narrows their items. For data from a server, [Async Options](./hooks/async-options.md) handles loading, debouncing, and aborting stale requests.
868
+
869
+ Data lists can virtualize large collections. You don't need to render every item yourself.
870
+
871
+ ### Define a Menu
872
+
873
+ A `MenuData` array describes actions, separators, groups, and submenus. A plain string is an action whose value and label are the string:
874
+
875
+ ```tsx
876
+ "use client"
877
+
878
+ import { Menu, MenuTrigger, type MenuData } from "@ui-sdk/react/menu"
879
+
880
+ const actions: MenuData = [
881
+ { value: "open", label: "Open", shortcut: "mod+o" },
882
+ { type: "separator" },
883
+ {
884
+ type: "submenu",
885
+ label: "Share",
886
+ items: [{ value: "email", label: "Email" }],
887
+ },
888
+ ]
889
+
890
+ function Example() {
891
+ return (
892
+ <Menu items={actions} onAction={(value) => console.log(value)}>
893
+ <MenuTrigger>File</MenuTrigger>
894
+ </Menu>
895
+ )
896
+ }
897
+
898
+ export { Example }
899
+ ```
900
+
901
+ To style it, put your own `MenuContent` in the menu and render its items from the same array, as [Menu](./components/menu.md#menus-as-data) shows. That page also covers checkbox items, radio groups, and nested menus.
902
+
903
+ ### Keys and Shortcuts
904
+
905
+ `loop` controls whether arrow keys wrap around a list. Components that support `orientation` use it to choose the navigation axis.
906
+
907
+ A trigger's `shortcut="mod+k"` opens it from elsewhere on the page. `mod` means Command on Apple platforms and Control elsewhere. A data menu runs item shortcuts while closed; a manually rendered MenuItem's shortcut only displays the keys.
908
+
909
+ [Shortcuts](./hooks/shortcuts.md) covers registering your own. Page shortcuts pause while a modal is open, and ordinary typing in text fields stays typing.
910
+
911
+ ## Forms
912
+
913
+ Give a control a `name` to include its value in a form. This works for Select and Combobox as well as inputs and checkboxes.
914
+
915
+ ### Submit a Value
916
+
917
+ ```tsx
918
+ import { Checkbox } from "@ui-sdk/react/checkbox"
919
+
920
+ function Example() {
921
+ return (
922
+ <form action="/preferences" method="post">
923
+ <label>
924
+ <Checkbox name="updates" value="yes" defaultChecked />
925
+ Email me updates
926
+ </label>
927
+ <button type="submit">Save</button>
928
+ <button type="reset">Reset</button>
929
+ </form>
930
+ )
931
+ }
932
+
933
+ export { Example }
934
+ ```
935
+
936
+ The checkbox submits `updates=yes` when checked. Reset restores its default value.
937
+
938
+ ### Reset a Controlled Value
939
+
940
+ A reset also goes through `onCheckedChange`. Update your React state there, just as you would after a click:
941
+
942
+ ```tsx
943
+ "use client"
944
+
945
+ import { Checkbox, type CheckedState } from "@ui-sdk/react/checkbox"
946
+ import { useState } from "react"
947
+
948
+ function Example() {
949
+ const [checked, setChecked] = useState<CheckedState>(true)
950
+ const [reason, setReason] = useState("No change yet")
951
+
952
+ return (
953
+ <form>
954
+ <label>
955
+ <Checkbox
956
+ name="updates"
957
+ defaultChecked
958
+ checked={checked}
959
+ onCheckedChange={(next, details) => {
960
+ setChecked(next)
961
+ setReason(details.reason)
962
+ }}
963
+ />
964
+ Email me updates
965
+ </label>
966
+ <button type="reset">Reset</button>
967
+ <output>{reason}</output>
968
+ </form>
969
+ )
970
+ }
971
+
972
+ export { Example }
973
+ ```
974
+
975
+ Uncheck the box, then press Reset. `defaultChecked` supplies the reset value, and the callback receives `true` with reason `"reset"`. The callback runs when reset requests a different value; it does not run if the checkbox is already at its default.
976
+
977
+ ### Label and Validate a Field
978
+
979
+ `Field` connects a control to its label, description, and error message:
980
+
981
+ ```tsx
982
+ import {
983
+ Field,
984
+ FieldDescription,
985
+ FieldError,
986
+ FieldLabel,
987
+ } from "@ui-sdk/react/field"
988
+ import { Input } from "@ui-sdk/react/input"
989
+
990
+ function Example() {
991
+ return (
992
+ <Field name="email">
993
+ <FieldLabel>Email</FieldLabel>
994
+ <Input type="email" required />
995
+ <FieldDescription>We'll only use it for receipts.</FieldDescription>
996
+ <FieldError />
997
+ </Field>
998
+ )
999
+ }
1000
+
1001
+ export { Example }
1002
+ ```
1003
+
1004
+ You can use a Textarea, Checkbox, Switch, Radio Group, or Select in place of the Input. [Field](./components/field.md) shows how they connect.
1005
+
1006
+ These props follow native form conventions on controls that support them:
1007
+
1008
+ | Prop | What It Does |
1009
+ | ---------- | --------------------------------------------------------------------------- |
1010
+ | `name` | Sets the submitted field name. Without one, no value is submitted. |
1011
+ | `required` | Requires a value before the form can submit. |
1012
+ | `disabled` | Prevents interaction and leaves the control out of the submission. |
1013
+ | `readOnly` | Prevents edits while keeping the control focusable and its value submitted. |
1014
+ | `form` | Connects a control to a form by ID when it's outside that form. |
1015
+
1016
+ ### Handle a Submission
1017
+
1018
+ On a normal submit, the browser checks constraints such as `required` and `type="email"` before running your handler.
1019
+
1020
+ Your server may still reject a valid-looking value. Pass `invalid` to the field and show the server’s message. When the user edits the field, clear the message and return `invalid` to `undefined` so the field uses native validation again. This example expects a `/api/preferences` endpoint:
1021
+
1022
+ ```tsx
1023
+ "use client"
1024
+
1025
+ import { Field, FieldError, FieldLabel } from "@ui-sdk/react/field"
1026
+ import { Input } from "@ui-sdk/react/input"
1027
+ import * as React from "react"
1028
+
1029
+ function Example() {
1030
+ const [error, setError] = React.useState("")
1031
+
1032
+ async function save(event: React.FormEvent<HTMLFormElement>) {
1033
+ event.preventDefault()
1034
+ const response = await fetch("/api/preferences", {
1035
+ method: "POST",
1036
+ body: new FormData(event.currentTarget),
1037
+ })
1038
+ setError(response.ok ? "" : "That email is already taken.")
1039
+ }
1040
+
1041
+ return (
1042
+ <form onSubmit={save}>
1043
+ <Field name="email" invalid={error ? true : undefined}>
1044
+ <FieldLabel>Email</FieldLabel>
1045
+ <Input type="email" required onValueChange={() => setError("")} />
1046
+ <FieldError>{(state) => error || state.message}</FieldError>
1047
+ </Field>
1048
+ <button type="submit">Save</button>
1049
+ </form>
1050
+ )
1051
+ }
1052
+
1053
+ export { Example }
1054
+ ```
1055
+
1056
+ Without a handler, the form submits through its `action`, including a React server action.
1057
+
1058
+ ## Server Rendering
1059
+
1060
+ ui-sdk parts render their initial HTML on the server, including ARIA state and keyboard tab stops. Hydration connects the event handlers in the browser.
1061
+
1062
+ ### Render from a Server Component
1063
+
1064
+ In an app with React Server Components, such as Next.js, you can render ui-sdk components directly from a server page. Their modules already include `"use client"`:
1065
+
1066
+ ```tsx
1067
+ import { Toggle } from "@ui-sdk/react/toggle"
1068
+
1069
+ function Page() {
1070
+ return <Toggle defaultPressed>Bold</Toggle>
1071
+ }
1072
+
1073
+ export { Page }
1074
+ ```
1075
+
1076
+ The server sends a button with `aria-pressed="true"`. It can toggle after hydration. Native HTML behavior, such as submitting a form, can work before hydration.
1077
+
1078
+ ### Wrappers in a Server Component
1079
+
1080
+ A wrapper that only renders markup can stay on the server. `renderElement` lets it support the same `render` prop as other parts:
1081
+
1082
+ ```tsx
1083
+ import { renderElement, type PartProps } from "@ui-sdk/react/primitive"
1084
+
1085
+ function Card({ render, ...props }: PartProps<"div">) {
1086
+ return renderElement("div", { ...props, "data-card": "" }, render)
1087
+ }
1088
+
1089
+ function Page() {
1090
+ return (
1091
+ <Card render={<section />} className="card">
1092
+ Your plan renews on May 1.
1093
+ </Card>
1094
+ )
1095
+ }
1096
+
1097
+ export { Card, Page }
1098
+ ```
1099
+
1100
+ The server renders a `section` with the card’s props.
1101
+
1102
+ Put custom parts built with `usePrimitive`, `usePart`, or `createParts` in a client module. A Server Component can then render those exported parts, just like `Toggle` above.
1103
+
1104
+ ### Add Your Own Client Boundary
1105
+
1106
+ If your component owns React state, callbacks, or render functions, put `"use client"` at the top of its entry file:
1107
+
1108
+ ```tsx title="mute-button.tsx"
1109
+ "use client"
1110
+
1111
+ import { Toggle } from "@ui-sdk/react/toggle"
1112
+ import { useState } from "react"
1113
+
1114
+ function MuteButton() {
1115
+ const [muted, setMuted] = useState(false)
1116
+
1117
+ return (
1118
+ <>
1119
+ <Toggle pressed={muted} onPressedChange={setMuted}>
1120
+ Mute
1121
+ </Toggle>
1122
+ <output>{muted ? "Sound is off" : "Sound is on"}</output>
1123
+ </>
1124
+ )
1125
+ }
1126
+
1127
+ export { MuteButton }
1128
+ ```
1129
+
1130
+ Render `MuteButton` from your server page. Clicking Mute changes its pressed state and the output; the button keeps the same accessible name.
1131
+
1132
+ Keep ordinary callbacks and render functions inside the client boundary. They cannot cross from a Server Component as props.
1133
+
1134
+ ### Saved Preferences and Storage
1135
+
1136
+ Preferences like a color scheme or an emoji skin tone are saved in the browser, so the server can't render them. [Preferences](./core.md#preferences) covers the preferences themselves; this is how they fit server rendering.
1137
+
1138
+ #### Show Them Before Hydration
1139
+
1140
+ Render `Preferences` at the top of `<body>`. On the server it renders one inline script as its first child, which puts every saved value on its `data-*` attribute as the page loads, before React hydrates, so CSS that selects those attributes is right from the first paint. Preferences hold functions, which can't cross from a Server Component as props, so give them to `Preferences` in a client module:
1141
+
1142
+ ```tsx title="app-preferences.tsx"
1143
+ "use client"
1144
+
1145
+ import { Preferences } from "@ui-sdk/react/preferences"
1146
+ import { colorScheme, skinTone } from "./preferences"
1147
+
1148
+ function AppPreferences({ children }: { children: React.ReactNode }) {
1149
+ return (
1150
+ <Preferences preferences={[colorScheme, skinTone]}>{children}</Preferences>
1151
+ )
1152
+ }
1153
+
1154
+ export { AppPreferences }
1155
+ ```
1156
+
1157
+ ```tsx title="root.tsx"
1158
+ import { AppPreferences } from "./app-preferences"
1159
+
1160
+ function Root({ children }: { children: React.ReactNode }) {
1161
+ return (
1162
+ <html lang="en" suppressHydrationWarning>
1163
+ <body>
1164
+ <AppPreferences>{children}</AppPreferences>
1165
+ </body>
1166
+ </html>
1167
+ )
1168
+ }
1169
+
1170
+ export { Root }
1171
+ ```
1172
+
1173
+ A preference made with `global: true` goes on `<html>`, which the script sets before hydration, so `<html>` needs `suppressHydrationWarning`. Pass `nonce` to `Preferences` if your Content Security Policy only allows inline scripts with one.
1174
+
1175
+ Content that reads a preference through `usePreference` renders the default on the server and the saved value right after hydration. Style by the attribute instead wherever the first paint has to be right, as [Style by Preferences](./core.md#style-by-preferences) shows.
1176
+
1177
+ #### Read a Cookie on the Server
1178
+
1179
+ A preference kept in a cookie can render on the server too, content included. Read the request's `Cookie` header on the server, however your server or framework gives you the request, and pass it to your root as a prop, like `<Root cookie={request.headers.get("cookie")}>`. A preference made from a request belongs to that request, so create it in the client boundary, once per render of the app, and share it through context:
1180
+
1181
+ ```tsx title="root.tsx"
1182
+ import { AppPreferences } from "./app-preferences"
1183
+
1184
+ function Root({
1185
+ cookie,
1186
+ children,
1187
+ }: {
1188
+ cookie: string | null
1189
+ children: React.ReactNode
1190
+ }) {
1191
+ return (
1192
+ <html lang="en" suppressHydrationWarning>
1193
+ <body>
1194
+ <AppPreferences cookie={cookie}>{children}</AppPreferences>
1195
+ </body>
1196
+ </html>
1197
+ )
1198
+ }
1199
+
1200
+ export { Root }
1201
+ ```
1202
+
1203
+ ```tsx title="app-preferences.tsx"
1204
+ "use client"
1205
+
1206
+ import { createCookieStorage, createPreference } from "@ui-sdk/core"
1207
+ import { Preferences } from "@ui-sdk/react/preferences"
1208
+ import { createContext, useState } from "react"
1209
+
1210
+ const createSkinTone = (cookie: string | null) =>
1211
+ createPreference({
1212
+ values: ["default", "light", "medium", "dark"],
1213
+ defaultValue: "default",
1214
+ attribute: "data-skin-tone",
1215
+ storage: createCookieStorage("skin-tone", { cookie }),
1216
+ })
1217
+
1218
+ const SkinToneContext = createContext<ReturnType<typeof createSkinTone> | null>(
1219
+ null
1220
+ )
1221
+
1222
+ function AppPreferences({
1223
+ cookie,
1224
+ children,
1225
+ }: {
1226
+ cookie: string | null
1227
+ children: React.ReactNode
1228
+ }) {
1229
+ const [skinTone] = useState(() => createSkinTone(cookie))
1230
+ return (
1231
+ <SkinToneContext value={skinTone}>
1232
+ <Preferences preferences={[skinTone]}>{children}</Preferences>
1233
+ </SkinToneContext>
1234
+ )
1235
+ }
1236
+
1237
+ export { AppPreferences, SkinToneContext }
1238
+ ```
1239
+
1240
+ The server reads the value from `cookie`, and the browser from `document.cookie`, so both render it and hydration matches. `usePreference(skinTone)` returns the saved value from the first render.
1241
+
1242
+ #### Render the Storage Script Yourself
1243
+
1244
+ `Preferences` is the usual way. For one component that keeps its own value, like a toolbar's density, a storage's `script` returns the same kind of inline script for a single element. Render it right after the element, since it writes to the element right before it:
1245
+
1246
+ ```tsx title="toolbar.tsx"
1247
+ "use client"
1248
+
1249
+ import { createLocalStorage } from "@ui-sdk/core"
1250
+ import { useEffect, useState, useSyncExternalStore } from "react"
1251
+
1252
+ const density = createLocalStorage("density")
1253
+ const script = density.script({
1254
+ attribute: "data-density",
1255
+ values: ["comfortable", "compact"],
1256
+ })
1257
+ const noSubscribe = () => () => {}
1258
+
1259
+ function Toolbar({ children }: { children: React.ReactNode }) {
1260
+ const [value, setValue] = useState("comfortable")
1261
+ // True on the server and while hydrating, when the script is in the HTML.
1262
+ const fromServer = useSyncExternalStore(
1263
+ noSubscribe,
1264
+ () => false,
1265
+ () => true
1266
+ )
1267
+ useEffect(
1268
+ () => setValue(density.get() === "compact" ? "compact" : "comfortable"),
1269
+ []
1270
+ )
1271
+ return (
1272
+ <>
1273
+ <div data-density={value} suppressHydrationWarning>
1274
+ {children}
1275
+ </div>
1276
+ {fromServer && script ? (
1277
+ <script dangerouslySetInnerHTML={{ __html: script }} />
1278
+ ) : null}
1279
+ </>
1280
+ )
1281
+ }
1282
+
1283
+ export { Toolbar }
1284
+ ```
1285
+
1286
+ - **Only from the server.** The browser never runs a script React inserts, and React warns about one, so leave it out of a render in the browser only. `fromServer` is true on the server and while hydrating, and false after.
1287
+ - **The element differs until hydration.** The script changes its attribute before React hydrates, so `suppressHydrationWarning` goes on that element. After hydration, render the saved value from state, as above.
1288
+ - **Shared by ID.** Every script with the same storage kind and ID reads the storage once, however many elements render it, and the scripts are identical, so compression shrinks the repeats.
1289
+ - **Only safe values.** Any script on your site can write the saved value, so `script` only sets attributes that start with `data-`, and only to one of `values`.
1290
+ - **Not for cookies.** A cookie storage's `script` returns undefined, since the server reads the cookie and renders the value itself.
1291
+
1292
+ To run before the page's content at all, render `preferencesScript([...])` from `@ui-sdk/core` in `<head>` yourself. Only global preferences make sense there, since the others go on the script's parent element.
1293
+
1294
+ ### Keep the First Render Consistent
1295
+
1296
+ Use the same initial values on the server and client. Read browser-only values in an effect or event handler, after hydration.
1297
+
1298
+ #### IDs
1299
+
1300
+ Parts generate IDs for their relationships, such as a Dialog trigger’s `aria-controls` and a Field label’s `for`. You usually do not need to supply them.
1301
+
1302
+ Pass an explicit `id` when something outside the component needs to refer to a part. On the server, render order matters: if a Field label renders before a control with your ID, the label initially uses the generated ID. Hydration updates the reference to yours. The same applies to Dialog triggers, titles, and descriptions.
1303
+
1304
+ Keep a part with your ID in the same Suspense boundary as the parts that reference it so they hydrate together. Pass the ID to the part or its `render` element; an ID assigned inside a render function cannot be read ahead and is picked up during hydration.
1305
+
1306
+ Use classes or data attributes for styling, rather than generated IDs.
1307
+
1308
+ Popups render in place, so they don't need a portal root in your server layout. [Popups](./popups.md) covers their placement and containers; [Forms](./forms.md) covers submission and validation.
1309
+
1310
+ ## Rendering Primitives
1311
+
1312
+ This page renders the rating from [Compose Behaviors](./core.md#compose-behaviors). Start with that guide's [complete `rating.ts`](./core.md#complete-primitive): it already handles selection, arrow keys, and focus. Here we'll connect its `root` and `star` slots to React.
1313
+
1314
+ ### Create the Parts
1315
+
1316
+ `createParts` maps slots to elements. Save this beside `rating.ts`:
1317
+
1318
+ ```ts title="rating-parts.ts"
1319
+ "use client"
1320
+
1321
+ import { createParts } from "@ui-sdk/react/primitive"
1322
+ import { rating } from "./rating"
1323
+
1324
+ const { Root: RatingRoot, Star: RatingStar } = createParts(rating, {
1325
+ root: "div",
1326
+ star: "button",
1327
+ })
1328
+
1329
+ export { RatingRoot, RatingStar }
1330
+ ```
1331
+
1332
+ `Root` creates the instance and shares it with the parts inside. Each mapped slot becomes a component. Slot names become Pascal case: `star` becomes `Star`, and `tab-icon` would become `TabIcon`.
1333
+
1334
+ If you leave out the `root` tag, `Root` still shares the instance but renders only its children.
1335
+
1336
+ ### Render the Rating
1337
+
1338
+ The root accepts the primitive's options and the element's props. Each star receives its slot value and a name a screen reader can announce:
1339
+
1340
+ ```tsx
1341
+ "use client"
1342
+
1343
+ import { RatingRoot, RatingStar } from "./rating-parts"
1344
+
1345
+ function Example() {
1346
+ return (
1347
+ <RatingRoot defaultValue={4} aria-label="Rating" className="flex gap-1">
1348
+ {Array.from({ length: 5 }, (_, index) => index + 1).map((star) => (
1349
+ <RatingStar
1350
+ key={star}
1351
+ value={String(star)}
1352
+ aria-label={`${star} ${star === 1 ? "star" : "stars"}`}
1353
+ className="opacity-40 data-filled:opacity-100"
1354
+ >
1355
+ ★
1356
+ </RatingStar>
1357
+ ))}
1358
+ </RatingRoot>
1359
+ )
1360
+ }
1361
+
1362
+ export { Example }
1363
+ ```
1364
+
1365
+ The primitive supplies `data-filled`; these classes make the filled stars easier to see. [Styling and Animation](./styling-and-animation.md) covers other ways to style parts.
1366
+
1367
+ Generated parts also support the same `render` prop and state callbacks as the components in the catalog. Use [Parts and Composition](./parts-and-composition.md) when you want to change their markup.
1368
+
1369
+ ### Wrap the Parts
1370
+
1371
+ If callers always want a row of stars, wrap the parts in one `Rating` component. This version adds `max` and `starLabel` while forwarding the primitive options and root element props. You can use it instead of the separate parts above.
1372
+
1373
+ ```tsx
1374
+ // rating-react.tsx
1375
+ "use client"
1376
+
1377
+ import { createParts, type PartProps } from "@ui-sdk/react/primitive"
1378
+ import { rating, type RatingOptions } from "./rating"
1379
+
1380
+ const parts = createParts(rating, { root: "div", star: "button" })
1381
+
1382
+ function Rating({
1383
+ max = 5,
1384
+ starLabel = (star) => `${star} ${star === 1 ? "star" : "stars"}`,
1385
+ ...props
1386
+ }: RatingOptions & {
1387
+ max?: number
1388
+ starLabel?: (star: number) => string
1389
+ } & Omit<PartProps<"div">, "defaultValue">) {
1390
+ return (
1391
+ <parts.Root {...props}>
1392
+ {Array.from({ length: max }, (_, index) => index + 1).map((star) => (
1393
+ <parts.Star
1394
+ key={star}
1395
+ value={String(star)}
1396
+ aria-label={starLabel(star)}
1397
+ >
1398
+ ★
1399
+ </parts.Star>
1400
+ ))}
1401
+ </parts.Root>
1402
+ )
1403
+ }
1404
+
1405
+ export { Rating }
1406
+ ```
1407
+
1408
+ ```tsx
1409
+ import { Rating } from "./rating-react"
1410
+
1411
+ function Example() {
1412
+ return (
1413
+ <Rating
1414
+ defaultValue={4}
1415
+ aria-label="Rating"
1416
+ className="flex gap-1 [&>button]:opacity-40 [&>[data-filled]]:opacity-100"
1417
+ />
1418
+ )
1419
+ }
1420
+
1421
+ export { Example }
1422
+ ```
1423
+
1424
+ Pass classes to the rendered parts. [Styling and Animation](./styling-and-animation.md) also shows how to use CSS Modules.
1425
+
1426
+ ### Add Logic to a Part
1427
+
1428
+ Keep `createParts` when mapping slots to elements is enough. [Custom Parts](./custom-parts.md) uses the same rating to show how `usePrimitive` and `usePart` let you write your own root or child part.
1429
+
1430
+ ## Custom Parts
1431
+
1432
+ The parts from [Rendering Primitives](./rendering.md) are enough until one needs its own logic. You can replace just that part and keep the same `rating.ts` definition from [Compose Behaviors](./core.md#complete-primitive).
1433
+
1434
+ ### Write a Root Part
1435
+
1436
+ Here's a replacement for the generated `RatingRoot`. `usePrimitive` creates its instance, and `usePart` connects the `root` slot:
1437
+
1438
+ ```tsx title="rating-root.tsx"
1439
+ "use client"
1440
+
1441
+ import type { StateOf } from "@ui-sdk/core"
1442
+ import { usePart, usePrimitive, type PartProps } from "@ui-sdk/react/primitive"
1443
+ import { rating, type RatingOptions } from "./rating"
1444
+
1445
+ function RatingRoot({
1446
+ value,
1447
+ defaultValue,
1448
+ onValueChange,
1449
+ ...props
1450
+ }: RatingOptions &
1451
+ Omit<PartProps<"div", StateOf<typeof rating, "root">>, "defaultValue">) {
1452
+ const handle = usePrimitive(rating, { value, defaultValue, onValueChange })
1453
+ return usePart(handle, "root", {}, props, "div")
1454
+ }
1455
+
1456
+ export { RatingRoot }
1457
+ ```
1458
+
1459
+ Pass primitive options to `usePrimitive` and element props to `usePart`. The empty `{}` is for slot props; this root doesn't need any. `usePart` shares the instance with child parts and renders nothing when the slot isn't present.
1460
+
1461
+ The adapter keeps options current and manages the instance's lifetime. Call `handle.api` from event handlers or effects. To render changing state, use a part's `children` or `render` function; reading `handle.state` alone doesn't subscribe to changes.
1462
+
1463
+ ### Connect a Child Part
1464
+
1465
+ A child can pass the primitive itself to `usePart`. It finds the instance shared by the root above it.
1466
+
1467
+ This `RatingStar` works inside the manual root above or the generated `RatingRoot`. It adds a default accessible label:
1468
+
1469
+ ```tsx
1470
+ "use client"
1471
+
1472
+ import { usePart, type PartProps } from "@ui-sdk/react/primitive"
1473
+ import { rating } from "./rating"
1474
+
1475
+ function RatingStar({
1476
+ value,
1477
+ ...props
1478
+ }: { value: string } & Omit<PartProps<"button">, "value">) {
1479
+ const label = `${value} ${value === "1" ? "star" : "stars"}`
1480
+ return usePart(
1481
+ rating,
1482
+ "star",
1483
+ { value },
1484
+ { "aria-label": label, ...props },
1485
+ "button"
1486
+ )
1487
+ }
1488
+
1489
+ export { RatingStar }
1490
+ ```
1491
+
1492
+ `{ value }` identifies the star. The remaining props go to its button. A caller can override the label with `aria-label`.
1493
+
1494
+ Use it in place of the generated star in [Render the Rating](./rendering.md#render-the-rating). Keep it inside `RatingRoot` so it can find the instance.
1495
+
1496
+ ### Merge Props
1497
+
1498
+ Use `mergeProps` when your part adds props that need to combine with the caller's. For example, an input can select its text on focus while still accepting an `onFocus` handler:
1499
+
1500
+ ```tsx
1501
+ "use client"
1502
+
1503
+ import {
1504
+ mergeProps,
1505
+ renderElement,
1506
+ type PartProps,
1507
+ } from "@ui-sdk/react/primitive"
1508
+ import type { FocusEvent } from "react"
1509
+
1510
+ function SelectOnFocusInput({ render, ...props }: PartProps<"input">) {
1511
+ const own = {
1512
+ onFocus: (event: FocusEvent<HTMLInputElement>) =>
1513
+ event.currentTarget.select(),
1514
+ }
1515
+ return renderElement("input", mergeProps(own, props), render)
1516
+ }
1517
+
1518
+ export { SelectOnFocusInput }
1519
+ ```
1520
+
1521
+ The caller's handler runs first and can call `preventDefault()` to skip yours. Class names join, styles merge, and refs combine. Other values from later arguments win; `undefined` doesn't replace an existing value.
1522
+
1523
+ For native `pointerenter` and `pointerleave` slot listeners, canceling React's handler doesn't cancel the slot's listener.
1524
+
1525
+ ### An Element Without a Primitive
1526
+
1527
+ `renderElement` also works for a plain wrapper. It gives the wrapper the same `render` prop as the other parts:
1528
+
1529
+ ```tsx
1530
+ "use client"
1531
+
1532
+ import { renderElement, type PartProps } from "@ui-sdk/react/primitive"
1533
+
1534
+ function Card({ render, ...props }: PartProps<"div">) {
1535
+ return renderElement("div", props, render)
1536
+ }
1537
+
1538
+ export { Card }
1539
+ ```
1540
+
1541
+ `PartProps` supplies the element props and `render`. Add its second type argument when `className`, `style`, or `children` should receive your part's state.
1542
+
1543
+ `renderElement` uses no hooks, so a wrapper like `Card` also works in a Server Component. Anything built on `usePrimitive`, `usePart` or `createParts` still needs a client component, since those use hooks. [Server Rendering](./server-rendering.md#wrappers-in-a-server-component) shows one.
1544
+
1545
+ ### Compose a Popup
1546
+
1547
+ A custom part can render an existing Popover or Dialog and connect its open state. [Compose Ours](./core.md#compose-ours) shows that connection with an emoji picker.
1548
+
1549
+ ## Components
1550
+
1551
+ Each page has the component's composition, examples, styling hooks and every prop.
1552
+
1553
+ - [Accordion](./components/accordion.md): Show and hide sections of content from their headings. Import from `@ui-sdk/react/accordion`.
1554
+ - [Alert Dialog](./components/alert-dialog.md): Ask someone to confirm an action before you continue. Import from `@ui-sdk/react/alert-dialog`.
1555
+ - [Autocomplete](./components/autocomplete.md): Suggest text while someone types, without limiting what they can enter. Import from `@ui-sdk/react/autocomplete`.
1556
+ - [Avatar](./components/avatar.md): Show an image with a fallback while it loads or if it fails. Import from `@ui-sdk/react/avatar`.
1557
+ - [Button](./components/button.md): Run an action, show that it's working, and keep it reachable when disabled. Import from `@ui-sdk/react/button`.
1558
+ - [Calendar](./components/calendar.md): Choose one date, several dates, or a range from a month grid. Import from `@ui-sdk/react/calendar`.
1559
+ - [Carousel](./components/carousel.md): Page through slides with native scrolling, keyboard controls, and optional rotation. Import from `@ui-sdk/react/carousel`.
1560
+ - [Checkbox](./components/checkbox.md): Choose one or more options, individually or as a group. Import from `@ui-sdk/react/checkbox`.
1561
+ - [Collapsible](./components/collapsible.md): Show and hide a panel, preserve its contents, and animate its height. Import from `@ui-sdk/react/collapsible`.
1562
+ - [Color Picker](./components/color-picker.md): Choose a color by dragging across an area and along sliders, typing any CSS color, or picking a preset. Import from `@ui-sdk/react/color-picker`.
1563
+ - [Combobox](./components/combobox.md): Search a list of options and choose one or several values. Import from `@ui-sdk/react/combobox`.
1564
+ - [Command](./components/command.md): Search commands, run actions, and open pages of more choices. Import from `@ui-sdk/react/command`.
1565
+ - [Date Input](./components/date-input.md): Type a date one part at a time, in the order your user's language writes it. Import from `@ui-sdk/react/date-input`.
1566
+ - [Date Picker](./components/date-picker.md): Type a date or choose one from a calendar, with support for ranges and forms. Import from `@ui-sdk/react/date-picker`.
1567
+ - [Dialog](./components/dialog.md): Open a focused task over the page. Import from `@ui-sdk/react/dialog`.
1568
+ - [Drawer](./components/drawer.md): A sheet you can open, drag, and resize from an edge of the screen. Import from `@ui-sdk/react/drawer`.
1569
+ - [Field](./components/field.md): Connect a form control to its label, description, and validation errors. Import from `@ui-sdk/react/field`.
1570
+ - [File Input](./components/file-input.md): Choose, drop, or paste files, validate them, and submit them with a form. Import from `@ui-sdk/react/file-input`.
1571
+ - [Hover Card](./components/hover-card.md): Preview a link on hover or keyboard focus while keeping its normal navigation. Import from `@ui-sdk/react/hover-card`.
1572
+ - [Image Cropper](./components/image-cropper.md): Choose the part of an image to keep by moving and resizing a selection, or by panning and zooming the image under it. Import from `@ui-sdk/react/image-cropper`.
1573
+ - [Input](./components/input.md): Enter text, add a clear button, and connect the value to your form or state. Import from `@ui-sdk/react/input`.
1574
+ - [Label](./components/label.md): Name any control with a label, and let a press on the label reach it. Import from `@ui-sdk/react/label`.
1575
+ - [Listbox](./components/listbox.md): Choose one or several options from an always-visible list. Import from `@ui-sdk/react/listbox`.
1576
+ - [Menu](./components/menu.md): Choose actions and settings from a button or a context menu. Import from `@ui-sdk/react/menu`.
1577
+ - [Menubar](./components/menubar.md): Move between an application’s menus with one tab stop. Import from `@ui-sdk/react/menubar`.
1578
+ - [Meter](./components/meter.md): Show a measurement within a known range, such as storage used or an account quota. Import from `@ui-sdk/react/meter`.
1579
+ - [Navigation Menu](./components/navigation-menu.md): Groups site links into panels that open from a row of triggers. Import from `@ui-sdk/react/navigation-menu`.
1580
+ - [Number Input](./components/number-input.md): Enter numbers, step through values, and format them for a locale. Import from `@ui-sdk/react/number-input`.
1581
+ - [OTP Input](./components/otp-input.md): Enter a one-time code with support for pasting, autofill, and native form validation. Import from `@ui-sdk/react/otp-input`.
1582
+ - [Pagination](./components/pagination.md): Navigate pages with buttons, links, or your router. Import from `@ui-sdk/react/pagination`.
1583
+ - [Password Input](./components/password-input.md): Enter a password, show or hide it, and use it in a native form. Import from `@ui-sdk/react/password-input`.
1584
+ - [Popover](./components/popover.md): Edit or explore something beside its trigger. Import from `@ui-sdk/react/popover`.
1585
+ - [Preferences](./components/preferences.md): Apply user preferences to data attributes so your styles can follow them. Import from `@ui-sdk/react/preferences`.
1586
+ - [Progress](./components/progress.md): Show a task’s progress, or an indeterminate state while its progress is unknown. Import from `@ui-sdk/react/progress`.
1587
+ - [Radio](./components/radio.md): Choose one option, move the selection with arrow keys, and submit it with a form. Import from `@ui-sdk/react/radio`.
1588
+ - [Rating](./components/rating.md): Pick a score from a row of stars, whole or half, with a preview as the pointer moves over them. Import from `@ui-sdk/react/rating`.
1589
+ - [Resizable](./components/resizable.md): Resize, collapse, and save panel layouts with pointer and keyboard controls. Import from `@ui-sdk/react/resizable`.
1590
+ - [Scroll Area](./components/scroll-area.md): Add styled scrollbars, edge indicators, and follow-to-end behavior to native scrolling. Import from `@ui-sdk/react/scroll-area`.
1591
+ - [Segmented Control](./components/segmented-control.md): Pick one of a few options from a row of buttons, with an indicator that slides to the choice. Import from `@ui-sdk/react/segmented-control`.
1592
+ - [Select](./components/select.md): Choose one value or several from a popup list. Import from `@ui-sdk/react/select`.
1593
+ - [Separator](./components/separator.md): Divide groups of content with a horizontal or vertical line. Import from `@ui-sdk/react/separator`.
1594
+ - [Slider](./components/slider.md): Adjust a number or a range with draggable thumbs and keyboard controls. Import from `@ui-sdk/react/slider`.
1595
+ - [Switch](./components/switch.md): Turn a setting on or off. Import from `@ui-sdk/react/switch`.
1596
+ - [Tabs](./components/tabs.md): Switch between related panels with one tab selected at a time. Import from `@ui-sdk/react/tabs`.
1597
+ - [Textarea](./components/textarea.md): Enter multiple lines of text, grow the field as you type, and connect it to a form. Import from `@ui-sdk/react/textarea`.
1598
+ - [Time Input](./components/time-input.md): Type a time one part at a time, on the clock your user's language uses.
1599
+ - [Timeline](./components/timeline.md): Show dated events in order, like an activity feed, a changelog or an order's progress, with arrow keys between them. Import from `@ui-sdk/react/timeline`.
1600
+ - [Toast](./components/toast.md): Show notifications with actions, timed dismissal, and screen reader announcements. Import from `@ui-sdk/react/toast`.
1601
+ - [Toggle](./components/toggle.md): Keep a button pressed, or group toggles with a shared value and arrow-key navigation. Import from `@ui-sdk/react/toggle`.
1602
+ - [Token Input](./components/token-input.md): Mix editable text with tokens for files, people, tags, and other references. Import from `@ui-sdk/react/token-input`.
1603
+ - [Toolbar](./components/toolbar.md): Give buttons, toggles, menus and fields one tab stop with arrow-key navigation. Import from `@ui-sdk/react/toolbar`.
1604
+ - [Tooltip](./components/tooltip.md): Name a control or add a short hint on hover and keyboard focus. Import from `@ui-sdk/react/tooltip`.
1605
+ - [Tree](./components/tree.md): Browse a hierarchy of files, pages, or choices with expandable branches. Import from `@ui-sdk/react/tree`.
1606
+ - [Trigger Menu](./components/trigger-menu.md): "Opens suggestions at the caret when you type a trigger such as @, /, or #." Import from `@ui-sdk/react/trigger-menu`.
1607
+ - [Waterfall](./components/waterfall.md): Show a trace or a page's requests as bars on a shared time axis, with spans that expand to show what they started. Import from `@ui-sdk/react/waterfall`.
1608
+
1609
+ ## Hooks
1610
+
1611
+ ### Async Options
1612
+
1613
+ `useAsyncOptions` loads items as someone types. It waits for typing to pause, cancels requests that are replaced, and keeps the previous results visible while loading.
1614
+
1615
+ Change the movie search and wait for the results. Use the arrow keys to choose a movie, then press Enter.
1616
+
1617
+ #### Load Results
1618
+
1619
+ Pass a loader to the hook. This example expects `/api/movies` to return an array of movie titles:
1620
+
1621
+ ```tsx
1622
+ "use client"
1623
+
1624
+ import { useAsyncOptions } from "@ui-sdk/react/async-options"
1625
+ import {
1626
+ Combobox,
1627
+ ComboboxContent,
1628
+ ComboboxInput,
1629
+ } from "@ui-sdk/react/combobox"
1630
+
1631
+ function MovieSearch() {
1632
+ const movies = useAsyncOptions<string>(async (query, { signal }) => {
1633
+ const response = await fetch(`/api/movies?q=${encodeURIComponent(query)}`, {
1634
+ signal,
1635
+ })
1636
+ if (!response.ok) throw new Error("Couldn't load movies")
1637
+ return response.json()
1638
+ })
1639
+
1640
+ return (
1641
+ <Combobox
1642
+ items={movies.items}
1643
+ filter={false}
1644
+ status={movies.status}
1645
+ onQueryChange={movies.setQuery}
1646
+ >
1647
+ <ComboboxInput aria-label="Movie" placeholder="Search movies…" />
1648
+ <ComboboxContent />
1649
+ </Combobox>
1650
+ )
1651
+ }
1652
+
1653
+ export { MovieSearch }
1654
+ ```
1655
+
1656
+ Pass `signal` to `fetch` so a newer search can cancel the request. The hook also cancels on unmount.
1657
+
1658
+ `filter={false}` leaves matching and result order to the server. `status` tells the component when it's busy. Use `ComboboxStatus` and `ComboboxEmpty` to show loading, error, and empty messages, as in the demo.
1659
+
1660
+ The same pattern works with [Select](./components/select.md#async-search), [Autocomplete](./components/autocomplete.md#async-suggestions), and [Command](./components/command.md#async-results).
1661
+
1662
+ #### Choose When to Load
1663
+
1664
+ The hook's second argument sets its starting behavior:
1665
+
1666
+ | Option | What It Does | Default |
1667
+ | ----------- | ------------------------------------------------ | ------- |
1668
+ | `debounce` | How long typing must pause, in milliseconds. | `200` |
1669
+ | `query` | The starting search text. | `""` |
1670
+ | `immediate` | Loads that text as soon as the component mounts. | `false` |
1671
+
1672
+ For example, add `{ immediate: true, debounce: 300 }` after the loader to fetch initial choices and wait a little longer between searches. Until that first load starts, `status` is already `"loading"`, so the list never flashes an empty state. If you set `query`, use the same starting text for the component's `defaultQuery`.
1673
+
1674
+ #### Load More
1675
+
1676
+ For paged results, the loader also receives `page`, starting at `0`. Return `{ items, hasMore }`:
1677
+
1678
+ ```tsx
1679
+ "use client"
1680
+
1681
+ import { useAsyncOptions } from "@ui-sdk/react/async-options"
1682
+
1683
+ function useMoviePages() {
1684
+ return useAsyncOptions<string>(async (query, { signal, page }) => {
1685
+ const response = await fetch(
1686
+ `/api/movies?q=${encodeURIComponent(query)}&page=${page}`,
1687
+ { signal }
1688
+ )
1689
+ if (!response.ok) throw new Error("Couldn't load movies")
1690
+ const data: { titles: string[]; next: number | null } =
1691
+ await response.json()
1692
+ return { items: data.titles, hasMore: data.next !== null }
1693
+ })
1694
+ }
1695
+
1696
+ export { useMoviePages }
1697
+ ```
1698
+
1699
+ Call `useMoviePages()` inside your component, then pass `onLoadMore={movies.loadMore}` to Combobox alongside the props above. It asks for another page when scrolling or keyboard navigation reaches the last item. The hook appends the new items.
1700
+
1701
+ Open the movie list and scroll down, or use Arrow Down to reach the last result. The next page is added to the list.
1702
+
1703
+ #### Retry a Search
1704
+
1705
+ `status` is `"loading"` while a request is pending, `"error"` after a failure, and `"idle"` otherwise. `error` holds the failure, and the previous items stay available.
1706
+
1707
+ Call `movies.reload()` from a retry button to load the current query again, starting at page `0`. `movies.hasMore` tells you whether another page is available.
1708
+
1709
+ ### Shortcuts
1710
+
1711
+ `useShortcut` registers a keyboard shortcut while your component is mounted. Use it for actions outside a particular control. For a component that already accepts `shortcut`, such as a dialog trigger, use that prop directly.
1712
+
1713
+ Press N to create a note. Press Command+S on Mac or Control+S elsewhere to save. The buttons run the same actions.
1714
+
1715
+ #### Register a Shortcut
1716
+
1717
+ Keys are joined with `+`. `mod` means Command on Apple platforms and Control elsewhere:
1718
+
1719
+ ```tsx
1720
+ "use client"
1721
+
1722
+ import { useShortcut } from "@ui-sdk/react/shortcuts"
1723
+ import { useState } from "react"
1724
+
1725
+ function NewNote() {
1726
+ const [count, setCount] = useState(0)
1727
+ const addNote = () => setCount((count) => count + 1)
1728
+ useShortcut("mod+enter", addNote, { description: "New note" })
1729
+
1730
+ return <button onClick={addNote}>New note ({count})</button>
1731
+ }
1732
+
1733
+ export { NewNote }
1734
+ ```
1735
+
1736
+ The hook uses your latest handler and unregisters on unmount. You don't need `useCallback`. Pass `null` instead of keys to leave a shortcut unregistered.
1737
+
1738
+ #### Show the Keys
1739
+
1740
+ `useShortcutHint` formats keys for the current platform and supplies `aria-keyshortcuts` for the control:
1741
+
1742
+ ```tsx
1743
+ "use client"
1744
+
1745
+ import { useShortcut, useShortcutHint } from "@ui-sdk/react/shortcuts"
1746
+
1747
+ function SaveButton({ onSave }: { onSave: () => void }) {
1748
+ useShortcut("mod+s", onSave, { description: "Save" })
1749
+ const hint = useShortcutHint("mod+s")
1750
+
1751
+ return (
1752
+ <button onClick={onSave} aria-keyshortcuts={hint.ariaKeyshortcuts}>
1753
+ Save <kbd aria-hidden="true">{hint.text}</kbd>
1754
+ </button>
1755
+ )
1756
+ }
1757
+
1758
+ export { SaveButton }
1759
+ ```
1760
+
1761
+ This shows `⌘S` on Apple platforms and `Ctrl+S` elsewhere. The hook keeps the server and first client render consistent, then updates after mount.
1762
+
1763
+ Outside rendering, use `formatShortcut` for display text or `ariaKeyshortcuts` for the ARIA value. Both are exported from the same entry. Compare the original keys, not the formatted hint.
1764
+
1765
+ #### While Someone Is Typing
1766
+
1767
+ Shortcuts with Command, Control, or Option can run in a text field. Plain letters and other keys stay available for typing.
1768
+
1769
+ Set `allowInInput: true` when the shortcut should work there too, such as Escape to clear a search. `when` checks a condition each time the keys are pressed; it can keep a Save shortcut inactive until there's text.
1770
+
1771
+ Focus the note and type some text. Press Command+Enter on Mac or Control+Enter elsewhere to save it, or Escape to clear it. Move focus outside the note to turn off its shortcuts.
1772
+
1773
+ #### Limit a Shortcut to an Area
1774
+
1775
+ Use a named `scope` for an editor or another part of the app. Turn it on while that area has focus:
1776
+
1777
+ ```tsx
1778
+ "use client"
1779
+
1780
+ import { shortcuts, useShortcut } from "@ui-sdk/react/shortcuts"
1781
+ import { useEffect, useState } from "react"
1782
+
1783
+ function Note() {
1784
+ const [text, setText] = useState("")
1785
+ useEffect(() => () => shortcuts.setScope("note", false), [])
1786
+ useShortcut("escape", () => setText(""), {
1787
+ scope: "note",
1788
+ allowInInput: true,
1789
+ })
1790
+
1791
+ return (
1792
+ <textarea
1793
+ aria-label="Note"
1794
+ value={text}
1795
+ onChange={(event) => setText(event.target.value)}
1796
+ onFocus={() => shortcuts.setScope("note", true)}
1797
+ onBlur={() => shortcuts.setScope("note", false)}
1798
+ />
1799
+ )
1800
+ }
1801
+
1802
+ export { Note }
1803
+ ```
1804
+
1805
+ A `when: () => text.length > 0` option can further limit when it runs. Scope names are shared, so choose one for each area you want to enable together.
1806
+
1807
+ #### Inside a Popup
1808
+
1809
+ Page shortcuts pause while a modal is open. Pass `layer` to make a shortcut belong to a popup instead:
1810
+
1811
+ ```tsx
1812
+ "use client"
1813
+
1814
+ import { DialogContent } from "@ui-sdk/react/dialog"
1815
+ import { useShortcut } from "@ui-sdk/react/shortcuts"
1816
+ import { useRef } from "react"
1817
+
1818
+ function EditorContent({ onSave }: { onSave: () => void }) {
1819
+ const content = useRef<HTMLDivElement>(null)
1820
+ useShortcut("mod+enter", onSave, { layer: content, description: "Save" })
1821
+
1822
+ return <DialogContent ref={content}>Edit your note here.</DialogContent>
1823
+ }
1824
+
1825
+ export { EditorContent }
1826
+ ```
1827
+
1828
+ Render this part inside a Dialog. Its shortcut runs only while that dialog is open. `layer` also accepts an element or a function that returns one.
1829
+
1830
+ When shortcuts share the same keys, the topmost open layer wins, then the most recently registered shortcut. Development builds warn about duplicate keys in the same place.
1831
+
1832
+ #### List Available Shortcuts
1833
+
1834
+ Call `shortcuts.list()` when a help screen opens. It returns the shortcuts that can run now, with their `keys`, formatted `hint`, and `description`:
1835
+
1836
+ ```ts
1837
+ import { shortcuts } from "@ui-sdk/react/shortcuts"
1838
+
1839
+ const labels = shortcuts
1840
+ .list()
1841
+ .map(({ hint, description }) => `${hint} ${description ?? ""}`)
1842
+ ```
1843
+
1844
+ Add descriptions when registering shortcuts so the help screen can explain each action.