@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.
- package/LICENSE +202 -0
- package/README.md +10 -2
- package/dist/accordion.d.ts +331 -0
- package/dist/accordion.js +25 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +8 -0
- package/dist/alert-dialog.d.ts +56 -0
- package/dist/alert-dialog.js +45 -0
- package/dist/async-options.d.ts +22 -0
- package/dist/async-options.js +56 -0
- package/dist/autocomplete.d.ts +102 -0
- package/dist/autocomplete.js +195 -0
- package/dist/avatar.d.ts +301 -0
- package/dist/avatar.js +17 -0
- package/dist/button.d.ts +21 -0
- package/dist/button.js +45 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +9 -0
- package/dist/calendar.d.ts +80 -0
- package/dist/calendar.js +257 -0
- package/dist/carousel.d.ts +52 -0
- package/dist/carousel.js +151 -0
- package/dist/checkbox.d.ts +24 -0
- package/dist/checkbox.js +98 -0
- package/dist/children.d.ts +38 -0
- package/dist/children.js +63 -0
- package/dist/collapsible.d.ts +302 -0
- package/dist/collapsible.js +19 -0
- package/dist/color-picker.d.ts +72 -0
- package/dist/color-picker.js +133 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +22 -0
- package/dist/combobox.d.ts +152 -0
- package/dist/combobox.js +338 -0
- package/dist/command.d.ts +88 -0
- package/dist/command.js +233 -0
- package/dist/controller-target.d.ts +3 -0
- package/dist/controller-target.js +59 -0
- package/dist/date-input.d.ts +19 -0
- package/dist/date-input.js +58 -0
- package/dist/date-picker.d.ts +47 -0
- package/dist/date-picker.js +310 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +60 -0
- package/dist/dialog-parts.d.ts +23 -0
- package/dist/dialog-parts.js +142 -0
- package/dist/dialog.d.ts +57 -0
- package/dist/dialog.js +32 -0
- package/dist/drawer.d.ts +70 -0
- package/dist/drawer.js +158 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +9 -0
- package/dist/field-context.d.ts +47 -0
- package/dist/field-context.js +87 -0
- package/dist/field.d.ts +49 -0
- package/dist/field.js +169 -0
- package/dist/file-input.d.ts +46 -0
- package/dist/file-input.js +170 -0
- package/dist/form-inputs.d.ts +5 -0
- package/dist/form-inputs.js +22 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +16 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +12 -0
- package/dist/hover-card.d.ts +33 -0
- package/dist/hover-card.js +84 -0
- package/dist/image-cropper.d.ts +333 -0
- package/dist/image-cropper.js +37 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +6 -0
- package/dist/input.d.ts +22 -0
- package/dist/input.js +51 -0
- package/dist/label.d.ts +294 -0
- package/dist/label.js +9 -0
- package/dist/list-items.d.ts +15 -0
- package/dist/list-items.js +39 -0
- package/dist/list-text.d.ts +4 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +56 -0
- package/dist/listbox.js +122 -0
- package/dist/menu.d.ts +116 -0
- package/dist/menu.js +426 -0
- package/dist/menubar-context.d.ts +4 -0
- package/dist/menubar-context.js +7 -0
- package/dist/menubar.d.ts +11 -0
- package/dist/menubar.js +32 -0
- package/dist/meter.d.ts +312 -0
- package/dist/meter.js +30 -0
- package/dist/navigation-menu.d.ts +61 -0
- package/dist/navigation-menu.js +169 -0
- package/dist/number-input.d.ts +44 -0
- package/dist/number-input.js +185 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +40 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +8 -0
- package/dist/otp-input.d.ts +29 -0
- package/dist/otp-input.js +94 -0
- package/dist/overlays.d.ts +26 -0
- package/dist/overlays.js +82 -0
- package/dist/pagination.d.ts +336 -0
- package/dist/pagination.js +71 -0
- package/dist/password-input.d.ts +23 -0
- package/dist/password-input.js +65 -0
- package/dist/popover.d.ts +54 -0
- package/dist/popover.js +151 -0
- package/dist/popup.d.ts +41 -0
- package/dist/popup.js +180 -0
- package/dist/preferences.d.ts +35 -0
- package/dist/preferences.js +93 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +14 -0
- package/dist/progress.d.ts +312 -0
- package/dist/progress.js +30 -0
- package/dist/radio.d.ts +22 -0
- package/dist/radio.js +86 -0
- package/dist/rating.d.ts +20 -0
- package/dist/rating.js +94 -0
- package/dist/render-element.d.ts +35 -0
- package/dist/render-element.js +103 -0
- package/dist/resizable.d.ts +302 -0
- package/dist/resizable.js +23 -0
- package/dist/scroll-area.d.ts +324 -0
- package/dist/scroll-area.js +49 -0
- package/dist/segmented-control.d.ts +24 -0
- package/dist/segmented-control.js +78 -0
- package/dist/segmented-input.d.ts +15 -0
- package/dist/segmented-input.js +48 -0
- package/dist/select.d.ts +125 -0
- package/dist/select.js +330 -0
- package/dist/separator.d.ts +290 -0
- package/dist/separator.js +9 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +61 -0
- package/dist/slider.d.ts +38 -0
- package/dist/slider.js +163 -0
- package/dist/switch.d.ts +15 -0
- package/dist/switch.js +39 -0
- package/dist/tabs.d.ts +331 -0
- package/dist/tabs.js +28 -0
- package/dist/text-control.d.ts +22 -0
- package/dist/text-control.js +24 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +8 -0
- package/dist/textarea.d.ts +10 -0
- package/dist/textarea.js +48 -0
- package/dist/time-input.d.ts +19 -0
- package/dist/time-input.js +63 -0
- package/dist/timeline.d.ts +327 -0
- package/dist/timeline.js +31 -0
- package/dist/toast-parts.d.ts +63 -0
- package/dist/toast-parts.js +144 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +22 -0
- package/dist/toggle.d.ts +23 -0
- package/dist/toggle.js +76 -0
- package/dist/token-input.d.ts +35 -0
- package/dist/token-input.js +100 -0
- package/dist/toolbar-context.d.ts +6 -0
- package/dist/toolbar-context.js +16 -0
- package/dist/toolbar.d.ts +38 -0
- package/dist/toolbar.js +63 -0
- package/dist/tooltip.d.ts +39 -0
- package/dist/tooltip.js +108 -0
- package/dist/tree.d.ts +51 -0
- package/dist/tree.js +150 -0
- package/dist/trigger-menu.d.ts +83 -0
- package/dist/trigger-menu.js +167 -0
- package/dist/use-primitive.d.ts +178 -0
- package/dist/use-primitive.js +674 -0
- package/dist/waterfall.d.ts +80 -0
- package/dist/waterfall.js +108 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1391 -0
- package/docs/components/alert-dialog.md +850 -0
- package/docs/components/autocomplete.md +1963 -0
- package/docs/components/avatar.md +252 -0
- package/docs/components/button.md +444 -0
- package/docs/components/calendar.md +1687 -0
- package/docs/components/carousel.md +1244 -0
- package/docs/components/checkbox.md +1069 -0
- package/docs/components/collapsible.md +567 -0
- package/docs/components/color-picker.md +736 -0
- package/docs/components/combobox.md +3647 -0
- package/docs/components/command.md +1573 -0
- package/docs/components/date-input.md +608 -0
- package/docs/components/date-picker.md +1266 -0
- package/docs/components/dialog.md +1469 -0
- package/docs/components/drawer.md +1197 -0
- package/docs/components/field.md +549 -0
- package/docs/components/file-input.md +609 -0
- package/docs/components/hover-card.md +514 -0
- package/docs/components/image-cropper.md +579 -0
- package/docs/components/input.md +301 -0
- package/docs/components/label.md +217 -0
- package/docs/components/listbox.md +1130 -0
- package/docs/components/menu.md +1286 -0
- package/docs/components/menubar.md +455 -0
- package/docs/components/meter.md +304 -0
- package/docs/components/navigation-menu.md +1581 -0
- package/docs/components/number-input.md +1086 -0
- package/docs/components/otp-input.md +565 -0
- package/docs/components/pagination.md +648 -0
- package/docs/components/password-input.md +737 -0
- package/docs/components/popover.md +1552 -0
- package/docs/components/preferences.md +220 -0
- package/docs/components/progress.md +443 -0
- package/docs/components/radio.md +846 -0
- package/docs/components/rating.md +458 -0
- package/docs/components/resizable.md +982 -0
- package/docs/components/scroll-area.md +980 -0
- package/docs/components/segmented-control.md +465 -0
- package/docs/components/select.md +2133 -0
- package/docs/components/separator.md +243 -0
- package/docs/components/slider.md +1169 -0
- package/docs/components/switch.md +613 -0
- package/docs/components/tabs.md +1014 -0
- package/docs/components/textarea.md +318 -0
- package/docs/components/time-input.md +231 -0
- package/docs/components/timeline.md +677 -0
- package/docs/components/toast.md +1395 -0
- package/docs/components/toggle.md +541 -0
- package/docs/components/token-input.md +1007 -0
- package/docs/components/toolbar.md +787 -0
- package/docs/components/tooltip.md +884 -0
- package/docs/components/tree.md +1346 -0
- package/docs/components/trigger-menu.md +1257 -0
- package/docs/components/waterfall.md +754 -0
- package/docs/controllers.md +89 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +120 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +148 -0
- package/docs/hooks/async-options.md +366 -0
- package/docs/hooks/shortcuts.md +221 -0
- package/docs/installation.md +45 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +170 -0
- package/docs/llms.txt +1844 -0
- package/docs/parts-and-composition.md +93 -0
- package/docs/popups.md +117 -0
- package/docs/rendering.md +121 -0
- package/docs/server-rendering.md +253 -0
- package/docs/state-and-events.md +105 -0
- package/docs/styling-and-animation.md +176 -0
- 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.
|