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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +561 -0
  4. package/dist/accordion.js +24 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +7 -0
  7. package/dist/alert-dialog.d.ts +231 -0
  8. package/dist/alert-dialog.js +33 -0
  9. package/dist/async-options.d.ts +18 -0
  10. package/dist/async-options.js +40 -0
  11. package/dist/autocomplete.d.ts +384 -0
  12. package/dist/autocomplete.js +344 -0
  13. package/dist/avatar.d.ts +40 -0
  14. package/dist/avatar.js +28 -0
  15. package/dist/button.d.ts +52 -0
  16. package/dist/button.js +43 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +7 -0
  19. package/dist/calendar.d.ts +311 -0
  20. package/dist/calendar.js +349 -0
  21. package/dist/carousel.d.ts +236 -0
  22. package/dist/carousel.js +204 -0
  23. package/dist/checkbox.d.ts +211 -0
  24. package/dist/checkbox.js +151 -0
  25. package/dist/children.d.ts +41 -0
  26. package/dist/children.js +135 -0
  27. package/dist/collapsible.d.ts +54 -0
  28. package/dist/collapsible.js +18 -0
  29. package/dist/color-picker.d.ts +187 -0
  30. package/dist/color-picker.js +158 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +21 -0
  33. package/dist/combobox.d.ts +517 -0
  34. package/dist/combobox.js +531 -0
  35. package/dist/command.d.ts +322 -0
  36. package/dist/command.js +345 -0
  37. package/dist/controller-target.d.ts +49 -0
  38. package/dist/controller-target.js +112 -0
  39. package/dist/create-parts.d.ts +126 -0
  40. package/dist/create-parts.js +206 -0
  41. package/dist/date-input.d.ts +133 -0
  42. package/dist/date-input.js +31 -0
  43. package/dist/date-picker.d.ts +328 -0
  44. package/dist/date-picker.js +354 -0
  45. package/dist/date.d.ts +2 -0
  46. package/dist/date.js +59 -0
  47. package/dist/dialog-parts.d.ts +259 -0
  48. package/dist/dialog-parts.js +300 -0
  49. package/dist/dialog.d.ts +251 -0
  50. package/dist/dialog.js +25 -0
  51. package/dist/drawer.d.ts +304 -0
  52. package/dist/drawer.js +373 -0
  53. package/dist/editor-context.d.ts +4 -0
  54. package/dist/editor-context.js +5 -0
  55. package/dist/field-context.d.ts +26 -0
  56. package/dist/field-context.js +44 -0
  57. package/dist/field.d.ts +154 -0
  58. package/dist/field.js +240 -0
  59. package/dist/file-input.d.ts +212 -0
  60. package/dist/file-input.js +255 -0
  61. package/dist/form-inputs.d.ts +16 -0
  62. package/dist/form-inputs.js +25 -0
  63. package/dist/gesture.d.ts +1 -0
  64. package/dist/gesture.js +15 -0
  65. package/dist/history.d.ts +2 -0
  66. package/dist/history.js +11 -0
  67. package/dist/hover-card.d.ts +137 -0
  68. package/dist/hover-card.js +153 -0
  69. package/dist/image-cropper.d.ts +74 -0
  70. package/dist/image-cropper.js +36 -0
  71. package/dist/image.d.ts +2 -0
  72. package/dist/image.js +5 -0
  73. package/dist/index.d.ts +59 -0
  74. package/dist/index.js +60 -0
  75. package/dist/input.d.ts +120 -0
  76. package/dist/input.js +73 -0
  77. package/dist/item-icon.d.ts +34 -0
  78. package/dist/item-icon.js +63 -0
  79. package/dist/label.d.ts +16 -0
  80. package/dist/label.js +8 -0
  81. package/dist/lifecycle.d.ts +11 -0
  82. package/dist/lifecycle.js +91 -0
  83. package/dist/list-items.d.ts +38 -0
  84. package/dist/list-items.js +59 -0
  85. package/dist/list-text.d.ts +7 -0
  86. package/dist/list-text.js +15 -0
  87. package/dist/listbox.d.ts +273 -0
  88. package/dist/listbox.js +126 -0
  89. package/dist/menu.d.ts +678 -0
  90. package/dist/menu.js +607 -0
  91. package/dist/menubar-context.d.ts +5 -0
  92. package/dist/menubar-context.js +5 -0
  93. package/dist/menubar.d.ts +61 -0
  94. package/dist/menubar.js +48 -0
  95. package/dist/meter.d.ts +41 -0
  96. package/dist/meter.js +37 -0
  97. package/dist/navigation-menu.d.ts +228 -0
  98. package/dist/navigation-menu.js +195 -0
  99. package/dist/number-input.d.ts +176 -0
  100. package/dist/number-input.js +231 -0
  101. package/dist/number.d.ts +2 -0
  102. package/dist/number.js +39 -0
  103. package/dist/nuxt/entries.json +977 -0
  104. package/dist/nuxt/module.d.ts +18 -0
  105. package/dist/nuxt/module.js +31 -0
  106. package/dist/nuxt/registrations.d.ts +22 -0
  107. package/dist/nuxt/registrations.js +22 -0
  108. package/dist/observe.d.ts +2 -0
  109. package/dist/observe.js +7 -0
  110. package/dist/otp-input.d.ts +142 -0
  111. package/dist/otp-input.js +130 -0
  112. package/dist/overlays.d.ts +40 -0
  113. package/dist/overlays.js +118 -0
  114. package/dist/pagination.d.ts +201 -0
  115. package/dist/pagination.js +146 -0
  116. package/dist/password-input.d.ts +144 -0
  117. package/dist/password-input.js +134 -0
  118. package/dist/popover.d.ts +211 -0
  119. package/dist/popover.js +258 -0
  120. package/dist/popup.d.ts +60 -0
  121. package/dist/popup.js +89 -0
  122. package/dist/preferences.d.ts +44 -0
  123. package/dist/preferences.js +102 -0
  124. package/dist/primitive.d.ts +2 -0
  125. package/dist/primitive.js +11 -0
  126. package/dist/progress.d.ts +41 -0
  127. package/dist/progress.js +37 -0
  128. package/dist/radio.d.ts +164 -0
  129. package/dist/radio.js +145 -0
  130. package/dist/rating.d.ts +149 -0
  131. package/dist/rating.js +146 -0
  132. package/dist/renderless.d.ts +4 -0
  133. package/dist/renderless.js +28 -0
  134. package/dist/resizable.d.ts +54 -0
  135. package/dist/resizable.js +22 -0
  136. package/dist/scroll-area.d.ts +72 -0
  137. package/dist/scroll-area.js +77 -0
  138. package/dist/segmented-control.d.ts +151 -0
  139. package/dist/segmented-control.js +110 -0
  140. package/dist/segmented-input.d.ts +76 -0
  141. package/dist/segmented-input.js +115 -0
  142. package/dist/select.d.ts +458 -0
  143. package/dist/select.js +468 -0
  144. package/dist/separator.d.ts +14 -0
  145. package/dist/separator.js +8 -0
  146. package/dist/shortcuts.d.ts +32 -0
  147. package/dist/shortcuts.js +56 -0
  148. package/dist/slider.d.ts +65 -0
  149. package/dist/slider.js +194 -0
  150. package/dist/switch.d.ts +103 -0
  151. package/dist/switch.js +71 -0
  152. package/dist/tabs.d.ts +338 -0
  153. package/dist/tabs.js +27 -0
  154. package/dist/text-control.d.ts +89 -0
  155. package/dist/text-control.js +51 -0
  156. package/dist/text-match.d.ts +2 -0
  157. package/dist/text-match.js +7 -0
  158. package/dist/textarea.d.ts +106 -0
  159. package/dist/textarea.js +68 -0
  160. package/dist/time-input.d.ts +131 -0
  161. package/dist/time-input.js +32 -0
  162. package/dist/timeline.d.ts +81 -0
  163. package/dist/timeline.js +30 -0
  164. package/dist/toast-parts.d.ts +203 -0
  165. package/dist/toast-parts.js +270 -0
  166. package/dist/toast.d.ts +2 -0
  167. package/dist/toast.js +21 -0
  168. package/dist/toggle-props.d.ts +22 -0
  169. package/dist/toggle-props.js +14 -0
  170. package/dist/toggle.d.ts +199 -0
  171. package/dist/toggle.js +129 -0
  172. package/dist/token-input.d.ts +136 -0
  173. package/dist/token-input.js +214 -0
  174. package/dist/toolbar-context.d.ts +7 -0
  175. package/dist/toolbar-context.js +20 -0
  176. package/dist/toolbar.d.ts +120 -0
  177. package/dist/toolbar.js +77 -0
  178. package/dist/tooltip.d.ts +160 -0
  179. package/dist/tooltip.js +224 -0
  180. package/dist/tree.d.ts +300 -0
  181. package/dist/tree.js +209 -0
  182. package/dist/trigger-menu.d.ts +452 -0
  183. package/dist/trigger-menu.js +254 -0
  184. package/dist/use-primitive.d.ts +132 -0
  185. package/dist/use-primitive.js +462 -0
  186. package/dist/waterfall.d.ts +201 -0
  187. package/dist/waterfall.js +171 -0
  188. package/docs/README.md +89 -0
  189. package/docs/agents.md +1523 -0
  190. package/docs/components/accordion.md +1369 -0
  191. package/docs/components/alert-dialog.md +799 -0
  192. package/docs/components/autocomplete.md +1857 -0
  193. package/docs/components/avatar.md +272 -0
  194. package/docs/components/button.md +435 -0
  195. package/docs/components/calendar.md +1589 -0
  196. package/docs/components/carousel.md +1171 -0
  197. package/docs/components/checkbox.md +1008 -0
  198. package/docs/components/collapsible.md +536 -0
  199. package/docs/components/color-picker.md +706 -0
  200. package/docs/components/combobox.md +3504 -0
  201. package/docs/components/command.md +1481 -0
  202. package/docs/components/date-input.md +536 -0
  203. package/docs/components/date-picker.md +1244 -0
  204. package/docs/components/dialog.md +1397 -0
  205. package/docs/components/drawer.md +1166 -0
  206. package/docs/components/field.md +538 -0
  207. package/docs/components/file-input.md +611 -0
  208. package/docs/components/hover-card.md +505 -0
  209. package/docs/components/image-cropper.md +556 -0
  210. package/docs/components/input.md +306 -0
  211. package/docs/components/label.md +203 -0
  212. package/docs/components/listbox.md +1004 -0
  213. package/docs/components/menu.md +1229 -0
  214. package/docs/components/menubar.md +443 -0
  215. package/docs/components/meter.md +315 -0
  216. package/docs/components/navigation-menu.md +1523 -0
  217. package/docs/components/number-input.md +1029 -0
  218. package/docs/components/otp-input.md +512 -0
  219. package/docs/components/pagination.md +613 -0
  220. package/docs/components/password-input.md +649 -0
  221. package/docs/components/popover.md +1448 -0
  222. package/docs/components/preferences.md +208 -0
  223. package/docs/components/progress.md +423 -0
  224. package/docs/components/radio.md +785 -0
  225. package/docs/components/rating.md +416 -0
  226. package/docs/components/resizable.md +910 -0
  227. package/docs/components/scroll-area.md +915 -0
  228. package/docs/components/segmented-control.md +435 -0
  229. package/docs/components/select.md +2015 -0
  230. package/docs/components/separator.md +229 -0
  231. package/docs/components/slider.md +1084 -0
  232. package/docs/components/switch.md +565 -0
  233. package/docs/components/tabs.md +944 -0
  234. package/docs/components/textarea.md +299 -0
  235. package/docs/components/time-input.md +224 -0
  236. package/docs/components/timeline.md +654 -0
  237. package/docs/components/toast.md +1224 -0
  238. package/docs/components/toggle.md +500 -0
  239. package/docs/components/token-input.md +953 -0
  240. package/docs/components/toolbar.md +739 -0
  241. package/docs/components/tooltip.md +818 -0
  242. package/docs/components/tree.md +1306 -0
  243. package/docs/components/trigger-menu.md +1223 -0
  244. package/docs/components/waterfall.md +717 -0
  245. package/docs/controllers.md +71 -0
  246. package/docs/core.md +8027 -0
  247. package/docs/custom-parts.md +89 -0
  248. package/docs/first-component.md +83 -0
  249. package/docs/forms.md +122 -0
  250. package/docs/installation.md +72 -0
  251. package/docs/introduction.md +32 -0
  252. package/docs/lists-and-menus.md +122 -0
  253. package/docs/llms.txt +1502 -0
  254. package/docs/parts-and-composition.md +85 -0
  255. package/docs/popups.md +108 -0
  256. package/docs/rendering.md +56 -0
  257. package/docs/server-rendering.md +180 -0
  258. package/docs/state-and-events.md +100 -0
  259. package/docs/styling-and-animation.md +148 -0
  260. package/docs/utilities/async-options.md +348 -0
  261. package/docs/utilities/shortcuts.md +159 -0
  262. package/package.json +352 -3
package/docs/llms.txt ADDED
@@ -0,0 +1,1502 @@
1
+ # ui-sdk for Vue
2
+
3
+ > @ui-sdk/vue renders ui-sdk's framework-free TypeScript core in Vue. How an interface behaves lives in the core; @ui-sdk/vue 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/vue: how Vue 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 Vue. Using another framework? See React, Svelte, Solid or Web Components. To build your own components from behaviors, see [Core](./core.md).
13
+
14
+ `@ui-sdk/vue` 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 Vue project. It needs Vue 3.5 or newer.
24
+
25
+ 1. Install @ui-sdk/vue 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/vue/docs/llms.txt. To build a new component, also search node_modules/@ui-sdk/vue/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 Vue 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 Vue 3.5 or newer.
44
+
45
+ ### Install the Packages
46
+
47
+ ```bash
48
+ npm install @ui-sdk/vue @ui-sdk/core
49
+ ```
50
+
51
+ `@ui-sdk/vue` provides the Vue 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
+ ### Import a Component
54
+
55
+ Import each component from its own entry:
56
+
57
+ ```ts
58
+ import { Button } from "@ui-sdk/vue/button"
59
+ import { Toggle } from "@ui-sdk/vue/toggle"
60
+ ```
61
+
62
+ A Dialog has separate parts for its trigger, content, and title. Import those parts from the same entry:
63
+
64
+ ```ts
65
+ import {
66
+ Dialog,
67
+ DialogContent,
68
+ DialogTitle,
69
+ DialogTrigger,
70
+ } from "@ui-sdk/vue/dialog"
71
+ ```
72
+
73
+ Each [component page](./README.md) lists the parts you can use.
74
+
75
+ ### Use with Nuxt
76
+
77
+ Install the same two packages in a Nuxt app, then add `@ui-sdk/vue` to its modules:
78
+
79
+ ```ts title="nuxt.config.ts"
80
+ export default defineNuxtConfig({
81
+ modules: ["@ui-sdk/vue"],
82
+ })
83
+ ```
84
+
85
+ Nuxt finds the module at `@ui-sdk/vue/nuxt`. It registers each part of every entry as an auto-imported component, like `<DialogTrigger>`, and auto-imports their other exports, like `createDialogController`, in `<script>`. Each one is still imported from its own entry, so a page bundles only what it uses. When a name clashes, your own component or `utils/` export wins. `confirm` is left out because it would replace the browser's `confirm()` in your code, so import it from `@ui-sdk/vue/alert-dialog`.
86
+
87
+ The module adds no plugin and no styles. Components render on the server and hydrate in the browser without one, so importing them from their entries, as above, works in Nuxt too.
88
+
89
+ Pass `prefix` to use ui-sdk's components beside your own of the same name:
90
+
91
+ ```ts title="nuxt.config.ts"
92
+ export default defineNuxtConfig({
93
+ modules: ["@ui-sdk/vue"],
94
+ uiSdk: { prefix: "U" },
95
+ })
96
+ ```
97
+
98
+ With that prefix, `<DialogTrigger>` becomes `<UDialogTrigger>`. Other exports keep their names.
99
+
100
+ ### Add Your Styles
101
+
102
+ 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.
103
+
104
+ You can render your first component without setting up a provider, importing a UI SDK stylesheet, or adding a portal root. [Server Rendering](./server-rendering.md) covers rendering the initial HTML and adding interactions during hydration, in Nuxt or your own server. If your Nuxt app sends a Content Security Policy, [Content Security Policy](./styling-and-animation.md#content-security-policy) covers nuxt-security.
105
+
106
+ ### Right to Left
107
+
108
+ Set `dir="rtl"` on the page or a container. Arrow keys and popup placement follow that direction without a provider. Use logical CSS properties, such as `padding-inline-start`, for your styles.
109
+
110
+ Continue with [First Component](./first-component.md) to add a working toggle to your app.
111
+
112
+ ## First Component
113
+
114
+ Let's add a Bookmark toggle. Press it once to turn it on, then again to turn it off.
115
+
116
+ If you haven't installed the packages yet, start with [Installation](./installation.md).
117
+
118
+ ### Create the Component
119
+
120
+ Create `bookmark-button.vue` next to your app component and render a `Toggle`:
121
+
122
+ ```vue title="bookmark-button.vue"
123
+ <script setup lang="ts">
124
+ import { Toggle } from "@ui-sdk/vue/toggle"
125
+ </script>
126
+
127
+ <template>
128
+ <Toggle>Bookmark</Toggle>
129
+ </template>
130
+ ```
131
+
132
+ `Toggle` renders a button. It keeps track of its pressed state, handles Enter and Space, and sets `aria-pressed` for screen readers.
133
+
134
+ ### Use It in Your App
135
+
136
+ In your app component (`App.vue` here), import and render `BookmarkButton`:
137
+
138
+ ```vue title="App.vue"
139
+ <script setup lang="ts">
140
+ import BookmarkButton from "./bookmark-button.vue"
141
+ </script>
142
+
143
+ <template>
144
+ <BookmarkButton />
145
+ </template>
146
+ ```
147
+
148
+ Open your app. You should see a button labeled Bookmark. Next, add styles so you can tell when it is pressed.
149
+
150
+ ### Add Styles
151
+
152
+ When the toggle is pressed, it has a `data-pressed` attribute. Use that attribute to change its appearance.
153
+
154
+ With Tailwind CSS configured in your app, add utility classes to `class`. The `data-pressed:` variant applies while the toggle is pressed:
155
+
156
+ ```vue title="bookmark-button.vue"
157
+ <script setup lang="ts">
158
+ import { Toggle } from "@ui-sdk/vue/toggle"
159
+ </script>
160
+
161
+ <template>
162
+ <Toggle
163
+ class="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"
164
+ >
165
+ Bookmark
166
+ </Toggle>
167
+ </template>
168
+ ```
169
+
170
+ Try the styled toggle:
171
+
172
+ ```vue title="first-component.vue"
173
+ <script setup lang="ts">
174
+ import { Toggle } from "@ui-sdk/vue/toggle"
175
+ </script>
176
+
177
+ <template>
178
+ <Toggle data-variant="outline"> Bookmark </Toggle>
179
+ </template>
180
+ ```
181
+
182
+ 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.
183
+
184
+ Keep the label the same in both states. `aria-pressed` tells a screen reader whether the toggle is on.
185
+
186
+ ### Keep Going
187
+
188
+ - [Toggle](./components/toggle.md) covers disabled states, controlled state, and groups of toggles.
189
+ - [Components](./README.md) lists the other components you can add with the same install.
190
+ - [Parts and Composition](./parts-and-composition.md) shows how to put a component with several parts together.
191
+ - [State and Events](./state-and-events.md) shows how to read changes and control state from your app.
192
+ - [Styling and Animation](./styling-and-animation.md) covers state attributes, CSS variables, and transitions.
193
+
194
+ ## Parts and Composition
195
+
196
+ A component can have several parts. A Popover has a root to hold its state, a trigger to open it, and content to show.
197
+
198
+ ### Put Parts Together
199
+
200
+ Import the parts you need and nest them as shown in the component's Composition:
201
+
202
+ ```vue
203
+ <script setup lang="ts">
204
+ import {
205
+ Popover,
206
+ PopoverClose,
207
+ PopoverContent,
208
+ PopoverTitle,
209
+ PopoverTrigger,
210
+ } from "@ui-sdk/vue/popover"
211
+ </script>
212
+
213
+ <template>
214
+ <Popover>
215
+ <PopoverTrigger>Details</PopoverTrigger>
216
+ <PopoverContent>
217
+ <PopoverTitle>Project Details</PopoverTitle>
218
+ <p>Last updated today.</p>
219
+ <PopoverClose>Done</PopoverClose>
220
+ </PopoverContent>
221
+ </Popover>
222
+ </template>
223
+ ```
224
+
225
+ `Popover` shares state without adding an element. Other roots, such as `Tabs`, do render an element. Parts find their root through the framework's context, so you can put your own components between them.
226
+
227
+ ### Choose the Element
228
+
229
+ Render a menu item as a link:
230
+
231
+ ```vue
232
+ <script setup lang="ts">
233
+ import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/vue/menu"
234
+ </script>
235
+
236
+ <template>
237
+ <Menu>
238
+ <MenuTrigger>Account</MenuTrigger>
239
+ <MenuContent>
240
+ <MenuItem href="/settings">
241
+ <template #render="{ props }">
242
+ <a v-bind="props">Settings</a>
243
+ </template>
244
+ </MenuItem>
245
+ </MenuContent>
246
+ </Menu>
247
+ </template>
248
+ ```
249
+
250
+ The `#render` slot receives the part's `props` and `state`. Bind the props with `v-bind="props"` to keep the part's behavior connected. It also receives `children`, the part's own content, like a slider thumb's range input: render it inside your element with `<component :is="children" />`. A component works too: the part binds to its root element, so give the component one element at its root.
251
+
252
+ Your event handler runs first. Calling `event.preventDefault()` skips the built-in handler. Keep the supplied roles, IDs, and ARIA state when rendering your own 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.
253
+
254
+ ### Read the State
255
+
256
+ The part can pass its state to its content:
257
+
258
+ ```vue
259
+ <script setup lang="ts">
260
+ import { Toggle } from "@ui-sdk/vue/toggle"
261
+ </script>
262
+
263
+ <template>
264
+ <Toggle v-slot="{ state }" aria-label="Mute">{{
265
+ state.pressed ? "Mute on" : "Mute off"
266
+ }}</Toggle>
267
+ </template>
268
+ ```
269
+
270
+ Keep the accessible name stable when the content changes. Here, `aria-label="Mute"` names the toggle and `aria-pressed` reports its state.
271
+
272
+ ### Name the Control
273
+
274
+ Use the component's label or title part, such as `FieldLabel` or `DialogTitle`. Give controls without visible text an `aria-label`.
275
+
276
+ [Custom Parts](./custom-parts.md) covers writing a part with logic of its own.
277
+
278
+ ## State and Events
279
+
280
+ Components manage their own state unless you pass a controlled value. A change callback lets you respond in either case.
281
+
282
+ ### Set the Starting Value
283
+
284
+ Use a default prop to choose the starting state. Here, Bold starts pressed and changes when you click it:
285
+
286
+ ```vue
287
+ <script setup lang="ts">
288
+ import { Toggle } from "@ui-sdk/vue/toggle"
289
+ </script>
290
+
291
+ <template>
292
+ <Toggle default-pressed>Bold</Toggle>
293
+ </template>
294
+ ```
295
+
296
+ The prop depends on the component: `default-pressed`, `default-checked`, `default-open`, or `default-value`.
297
+
298
+ ### Control the Value
299
+
300
+ Use `v-model` when the parent needs the current value:
301
+
302
+ ```vue
303
+ <script setup lang="ts">
304
+ import { Toggle } from "@ui-sdk/vue/toggle"
305
+ import { ref } from "vue"
306
+
307
+ const pressed = ref(false)
308
+ </script>
309
+
310
+ <template>
311
+ <Toggle v-model="pressed">Bold</Toggle>
312
+ <output>{{ pressed ? "On" : "Off" }}</output>
313
+ </template>
314
+ ```
315
+
316
+ `v-model` keeps the toggle’s `pressed` value in your ref. `v-model:pressed` works here too. Clicking Bold changes both the button and the output.
317
+
318
+ The main value depends on the component: `checked` for Checkbox, `open` for Popover, and `value` for Select. Name another binding when you need it, such as `v-model:open` on Select or `v-model:query` on Combobox.
319
+
320
+ If you pass `:pressed` without `v-model`, update that value yourself in `@pressed-change`. A value the parent never updates stays fixed.
321
+
322
+ To keep a value in the URL, update your ref through `v-model` or the event, then call `router.push`. A value that changes only when the navigation finishes leaves the component showing the old one until then, and logs a warning in development.
323
+
324
+ ### Respond to a Change
325
+
326
+ Events such as `@pressed-change` receive the requested value and `details`. They run before `v-model` updates.
327
+
328
+ | Detail | What It Tells You |
329
+ | ---------- | ------------------------------------------------------------------------------------------------ |
330
+ | `reason` | What requested the change, such as `press` or `outside-press`. Each component lists its reasons. |
331
+ | `event` | The DOM event, when there is one. |
332
+ | `source` | `keyboard`, `mouse`, `touch`, `pen`, `virtual`, `programmatic`, or `agent`. |
333
+ | `cancel()` | Stops the requested change. |
334
+
335
+ For example, keep a popover open when someone clicks outside it:
336
+
337
+ ```vue
338
+ <script setup lang="ts">
339
+ import {
340
+ Popover,
341
+ PopoverClose,
342
+ PopoverContent,
343
+ PopoverTitle,
344
+ PopoverTrigger,
345
+ } from "@ui-sdk/vue/popover"
346
+
347
+ function keepOpen(
348
+ open: boolean,
349
+ details: { reason: string; cancel: () => void }
350
+ ) {
351
+ if (!open && details.reason === "outside-press") {
352
+ details.cancel()
353
+ }
354
+ }
355
+ </script>
356
+
357
+ <template>
358
+ <Popover @open-change="keepOpen">
359
+ <PopoverTrigger>Details</PopoverTrigger>
360
+ <PopoverContent>
361
+ <PopoverTitle>Project Details</PopoverTitle>
362
+ <p>Last updated today.</p>
363
+ <PopoverClose>Done</PopoverClose>
364
+ </PopoverContent>
365
+ </Popover>
366
+ </template>
367
+ ```
368
+
369
+ The Done button still closes it. Cancellation also works when you control the value. Add `debug` to a root to log changes while you work.
370
+
371
+ ### Empty and Multiple Values
372
+
373
+ 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.
374
+
375
+ To change a component from outside its parts, use a [Controller](./controllers.md).
376
+
377
+ ## Styling and Animation
378
+
379
+ 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.
380
+
381
+ ### Style a Part
382
+
383
+ Every part has a `data-slot` attribute. State adds attributes such as `data-pressed` and `data-disabled`:
384
+
385
+ ```vue
386
+ <script setup lang="ts">
387
+ import { Toggle } from "@ui-sdk/vue/toggle"
388
+ </script>
389
+
390
+ <template>
391
+ <Toggle
392
+ class="rounded-md border border-neutral-300 px-2.5 py-1.5 text-sm data-pressed:bg-neutral-100 data-disabled:opacity-50"
393
+ >Bold</Toggle
394
+ >
395
+ </template>
396
+ ```
397
+
398
+ Vue's `class` and `style` bindings accept strings, arrays, and objects. They aren't functions of the part's state. Use data attributes, or read `state` in a `#render` slot.
399
+
400
+ Most state attributes are present or absent. Others have values, such as `data-side="top"`. Each component's Styling section lists its attributes and CSS variables. Use those rather than generated IDs.
401
+
402
+ ### CSS Modules
403
+
404
+ Import a CSS Module and pass its class to the part. State selectors work the same way:
405
+
406
+ ```vue
407
+ <script setup lang="ts">
408
+ import { Toggle } from "@ui-sdk/vue/toggle"
409
+ import styles from "./toggle.module.css"
410
+ </script>
411
+
412
+ <template>
413
+ <Toggle :class="styles.toggle">Bold</Toggle>
414
+ </template>
415
+ ```
416
+
417
+ ```css title="toggle.module.css"
418
+ .toggle {
419
+ cursor: default;
420
+ border: 1px solid var(--input, #e5e5e5);
421
+ border-radius: 1.125rem;
422
+ background: transparent;
423
+ padding: 0.375rem 0.625rem;
424
+ color: inherit;
425
+ font: inherit;
426
+ font-size: 0.875rem;
427
+ }
428
+
429
+ .toggle[data-pressed] {
430
+ background: var(--muted, #f5f5f5);
431
+ }
432
+
433
+ .toggle[data-disabled] {
434
+ opacity: 0.5;
435
+ }
436
+
437
+ .toggle:focus-visible {
438
+ outline: 2px solid var(--ring, #a3a3a3);
439
+ outline-offset: 2px;
440
+ }
441
+ ```
442
+
443
+ ### Animate Opening and Closing
444
+
445
+ Popup content gets `data-starting-style` before it enters and `data-ending-style` while it exits. It stays mounted until the exit transition or animation finishes:
446
+
447
+ ```vue
448
+ <script setup lang="ts">
449
+ import {
450
+ Popover,
451
+ PopoverClose,
452
+ PopoverContent,
453
+ PopoverTitle,
454
+ PopoverTrigger,
455
+ } from "@ui-sdk/vue/popover"
456
+ </script>
457
+
458
+ <template>
459
+ <Popover>
460
+ <PopoverTrigger>Details</PopoverTrigger>
461
+ <PopoverContent
462
+ class="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"
463
+ >
464
+ <PopoverTitle>Project Details</PopoverTitle>
465
+ <p>Last updated today.</p>
466
+ <PopoverClose>Done</PopoverClose>
467
+ </PopoverContent>
468
+ </Popover>
469
+ </template>
470
+ ```
471
+
472
+ Put exit styles on `data-ending-style`. `data-closed` describes the state but doesn't keep the element around for an exit.
473
+
474
+ Popups provide `--anchor-width` and `--available-height` for sizing. [Popups](./popups.md) covers mounting and placement.
475
+
476
+ ### The ui-sdk Layer
477
+
478
+ 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 `main.ts` or `nuxt.config.ts` loads, so your own styles win over it wherever the page puts them. With Tailwind, put it above `@import "tailwindcss"`:
479
+
480
+ ```css
481
+ @layer ui-sdk;
482
+ ```
483
+
484
+ A component's `<style>` block loads with the component, too late to come first, so keep the line in the global stylesheet.
485
+
486
+ 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:
487
+
488
+ ```css
489
+ @layer ui-sdk {
490
+ :where([data-slot][hidden]:not([hidden="until-found"])) {
491
+ display: none !important;
492
+ }
493
+ }
494
+ ```
495
+
496
+ It leaves `hidden="until-found"` alone, so find in page can still open a closed panel.
497
+
498
+ ### Content Security Policy
499
+
500
+ 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>`:
501
+
502
+ ```html
503
+ <meta property="csp-nonce" nonce="your-nonce" />
504
+ ```
505
+
506
+ 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.
507
+
508
+ In Nuxt, the nuxt-security module writes the tag only in development. For production, add a Nitro plugin, like `server/plugins/csp-nonce.ts`, that writes it into every page with the module's nonce:
509
+
510
+ ```ts
511
+ export default defineNitroPlugin((nitroApp) => {
512
+ nitroApp.hooks.hook("render:html", (html, { event }) => {
513
+ html.head.push(
514
+ `<meta property="csp-nonce" nonce="${event.context.security.nonce}">`
515
+ )
516
+ })
517
+ })
518
+ ```
519
+
520
+ 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.
521
+
522
+ 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. In Nuxt, `useHead` leaves out a meta tag without `content`, so give it one. ui-sdk doesn't read it: `useHead({ meta: [{ property: "csp-nonce", content: "none" }] })`. If your code replaces `document.adoptedStyleSheets`, ui-sdk adds its sheets back the next time a component mounts.
523
+
524
+ ## Controllers
525
+
526
+ A controller connects code to a mounted component. Use one when the button or action that changes it lives somewhere else.
527
+
528
+ ### Connect a Controller
529
+
530
+ Create it once in your component and pass it to the root:
531
+
532
+ ```vue
533
+ <script setup lang="ts">
534
+ import { createToggleController, Toggle } from "@ui-sdk/vue/toggle"
535
+
536
+ const toggle = createToggleController()
537
+ </script>
538
+
539
+ <template>
540
+ <button @click="() => toggle.toggle()">Toggle Bold</button>
541
+ <Toggle :controller="toggle">Bold</Toggle>
542
+ </template>
543
+ ```
544
+
545
+ Controller methods run the same change callbacks as user input. Controlled values and cancellation follow [State and Events](./state-and-events.md).
546
+
547
+ Each component lists its methods, such as `open()`, `close()`, `setValue()`, or `focus()`.
548
+
549
+ 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 the template doesn't update when the state changes. To render the state, keep it in a ref with `v-model:pressed`.
550
+
551
+ ### Share a Popup
552
+
553
+ External triggers can use the same controller. A `payload` tells the content which item was opened:
554
+
555
+ ```vue
556
+ <script setup lang="ts">
557
+ import {
558
+ createPopoverController,
559
+ Popover,
560
+ PopoverContent,
561
+ PopoverTitle,
562
+ PopoverTrigger,
563
+ } from "@ui-sdk/vue/popover"
564
+
565
+ const editor = createPopoverController<string>()
566
+ </script>
567
+
568
+ <template>
569
+ <PopoverTrigger :controller="editor" payload="Notes"
570
+ >Edit Notes</PopoverTrigger
571
+ >
572
+ <PopoverTrigger :controller="editor" payload="Tasks"
573
+ >Edit Tasks</PopoverTrigger
574
+ >
575
+ <Popover :controller="editor">
576
+ <PopoverContent v-slot="{ state }"
577
+ ><PopoverTitle>Edit {{ state.payload }}</PopoverTitle></PopoverContent
578
+ >
579
+ </Popover>
580
+ </template>
581
+ ```
582
+
583
+ ### Wait for a Popup to Close
584
+
585
+ A popup controller's `open()` returns a promise. It settles when the popup closes, with its reason and return value:
586
+
587
+ ```ts
588
+ const result = await editor.open({ payload: "Notes" })
589
+ console.log(result.reason, result.returnValue)
590
+ ```
591
+
592
+ For confirmations, [Alert Dialog](./components/alert-dialog.md) provides `createConfirm()`, which gives you a function you can await.
593
+
594
+ ## Popups
595
+
596
+ 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.
597
+
598
+ ### Place the Content
599
+
600
+ Set placement on the content part:
601
+
602
+ ```vue
603
+ <script setup lang="ts">
604
+ import {
605
+ Popover,
606
+ PopoverClose,
607
+ PopoverContent,
608
+ PopoverTitle,
609
+ PopoverTrigger,
610
+ } from "@ui-sdk/vue/popover"
611
+ </script>
612
+
613
+ <template>
614
+ <Popover>
615
+ <PopoverTrigger>Details</PopoverTrigger>
616
+ <PopoverContent side="bottom" align="start" :side-offset="8">
617
+ <PopoverTitle>Project Details</PopoverTitle>
618
+ <p>Last updated today.</p>
619
+ <PopoverClose>Done</PopoverClose>
620
+ </PopoverContent>
621
+ </Popover>
622
+ </template>
623
+ ```
624
+
625
+ The popup flips or shifts when there isn't enough room.
626
+
627
+ | Prop | What It Does |
628
+ | ------------------- | ---------------------------------------------------------------------------------------------------- |
629
+ | `side` | Chooses `top`, `bottom`, `left`, `right`, `start`, or `end`. Start and end follow reading direction. |
630
+ | `align` | Aligns the popup to the start, center, or end of that side. |
631
+ | `side-offset` | Sets the gap from the anchor, in pixels. |
632
+ | `align-offset` | Shifts it along its side. |
633
+ | `collision-padding` | Leaves space from the window's edges. |
634
+
635
+ An `anchor` positions the popup against another element. The component's page lists the anchors it accepts.
636
+
637
+ ### Control Dismissal
638
+
639
+ Set `dismissible` to false to stop Escape, outside presses, focus leaving, Back, and sheet swipes from closing it. The trigger, Close part, and controller can still close it:
640
+
641
+ ```vue
642
+ <script setup lang="ts">
643
+ import {
644
+ Popover,
645
+ PopoverClose,
646
+ PopoverContent,
647
+ PopoverTitle,
648
+ PopoverTrigger,
649
+ } from "@ui-sdk/vue/popover"
650
+ </script>
651
+
652
+ <template>
653
+ <Popover :dismissible="false">
654
+ <PopoverTrigger>Details</PopoverTrigger>
655
+ <PopoverContent>
656
+ <PopoverTitle>Project Details</PopoverTitle>
657
+ <p>Last updated today.</p>
658
+ <PopoverClose>Done</PopoverClose>
659
+ </PopoverContent>
660
+ </Popover>
661
+ </template>
662
+ ```
663
+
664
+ Use `@dismiss-attempt` to hear each refused dismissal. [State and Events](./state-and-events.md) shows how to cancel just one reason.
665
+
666
+ ### Modals and Sheets
667
+
668
+ `modal` set to true makes the rest of the page inert and locks scrolling. Each component documents its default.
669
+
670
+ 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.
671
+
672
+ ### Focus
673
+
674
+ Dialogs, alert dialogs, drawers, and popovers move focus into their content when they open. Where it lands depends on how they opened:
675
+
676
+ | Opened With | Focus Goes To |
677
+ | -------------------------------------------- | ------------------------------------------------------------ |
678
+ | Keyboard, screen reader, mouse, pen, or code | The element `initial-focus` picks. |
679
+ | Touch | The content itself, so no on-screen keyboard appears. |
680
+ | Hover | Nowhere. Focus stays where it is. |
681
+ | Rendered open | Nowhere, except in a dialog, which takes focus as it mounts. |
682
+
683
+ `initial-focus` 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.
684
+
685
+ `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.
686
+
687
+ 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 `final-focus` 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.
688
+
689
+ Menus, selects, and comboboxes handle focus within their own lists, and tooltips never move it. Each component's page covers its own.
690
+
691
+ ### Mounting and Containers
692
+
693
+ Closed content unmounts after its exit animation. `keep-mounted` leaves it in the page, hidden. `@open-change-complete` runs when opening or closing finishes, including animations.
694
+
695
+ 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.
696
+
697
+ In a page `<KeepAlive>` caches, an open popup leaves with the page, from its `container` too, and comes back open when the page does, with focus in it if it's modal.
698
+
699
+ [Styling and Animation](./styling-and-animation.md) covers transitions. For toasts and confirms, mount `OverlayHost` once near your app's root; [Toast](./components/toast.md) and [Alert Dialog](./components/alert-dialog.md) show the setup.
700
+
701
+ ## Lists and Menus
702
+
703
+ 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.
704
+
705
+ ### Start with Strings
706
+
707
+ ```vue
708
+ <script setup lang="ts">
709
+ import {
710
+ Select,
711
+ SelectContent,
712
+ SelectTrigger,
713
+ SelectValue,
714
+ } from "@ui-sdk/vue/select"
715
+ </script>
716
+
717
+ <template>
718
+ <Select :items="['Apple', 'Banana', 'Pear']">
719
+ <SelectTrigger aria-label="Fruit"
720
+ ><SelectValue placeholder="Choose a fruit"
721
+ /></SelectTrigger>
722
+ <SelectContent />
723
+ </Select>
724
+ </template>
725
+ ```
726
+
727
+ Each string is both the value and its label. Use objects when those need to differ:
728
+
729
+ ```ts
730
+ const items = [
731
+ { value: "apple", label: "Apple" },
732
+ { value: "banana", label: "Banana", disabled: true },
733
+ ]
734
+ ```
735
+
736
+ Items can have `keywords` for searching. Group them with `{ type: "group", label, items }`, or separate them with `{ type: "separator" }`.
737
+
738
+ For object values, provide `item-to-value` for the submitted string and `item-to-label` for the displayed text. [Select](./components/select.md) and [Combobox](./components/combobox.md) show complete examples.
739
+
740
+ ### Style the Items
741
+
742
+ Items as data render bare parts, each with just its label. To style them, render the parts yourself from your array, with your classes right on them. Type the array with `ListData`, so a mistake in it shows up where you write it:
743
+
744
+ ```vue
745
+ <script setup lang="ts">
746
+ import {
747
+ Select,
748
+ SelectContent,
749
+ SelectItem,
750
+ SelectTrigger,
751
+ SelectValue,
752
+ type ListData,
753
+ } from "@ui-sdk/vue/select"
754
+
755
+ const fruits = [
756
+ { value: "apple", label: "Apple" },
757
+ { value: "banana", label: "Banana" },
758
+ ] satisfies ListData
759
+ </script>
760
+
761
+ <template>
762
+ <Select :items="fruits">
763
+ <SelectTrigger aria-label="Fruit" class="…"
764
+ ><SelectValue placeholder="Choose a fruit"
765
+ /></SelectTrigger>
766
+ <SelectContent class="…">
767
+ <SelectItem
768
+ v-for="fruit in fruits"
769
+ :key="fruit.value"
770
+ :value="fruit.value"
771
+ class="…"
772
+ >
773
+ {{ fruit.label }}
774
+ </SelectItem>
775
+ </SelectContent>
776
+ </Select>
777
+ </template>
778
+ ```
779
+
780
+ A list that searches still filters the items you render, and hides the ones that don't match. When the items come from state, like results from a server, the list part's slot gives you the ones to show. Write `<SelectList v-slot="{ state }">` and render `state.items`.
781
+
782
+ ### Search and Load Items
783
+
784
+ Searchable components accept a `filter` to change how typing narrows their items. [Async Options](./utilities/async-options.md) handles server results, debouncing, and stale requests.
785
+
786
+ Data lists can virtualize large collections. You don't need to render every item yourself.
787
+
788
+ ### Describe a Menu
789
+
790
+ A `MenuData` array describes actions, separators, groups, and submenus. A plain string is an action whose value and label are the string:
791
+
792
+ ```vue
793
+ <script setup lang="ts">
794
+ import { Menu, MenuTrigger, type MenuData } from "@ui-sdk/vue/menu"
795
+
796
+ const actions: MenuData = [
797
+ { value: "open", label: "Open", shortcut: "mod+o" },
798
+ { type: "separator" },
799
+ {
800
+ type: "submenu",
801
+ label: "Share",
802
+ items: [{ value: "email", label: "Email" }],
803
+ },
804
+ ]
805
+ </script>
806
+
807
+ <template>
808
+ <Menu :items="actions" @action="(value: string) => console.log(value)">
809
+ <MenuTrigger>File</MenuTrigger>
810
+ </Menu>
811
+ </template>
812
+ ```
813
+
814
+ A data menu renders bare parts too. To style it, place a `MenuContent` of your own and render the items in it, as [Menu](./components/menu.md#menus-as-data) shows. That page also covers checkbox items, radio groups, and nested menus.
815
+
816
+ ### Keys and Shortcuts
817
+
818
+ `loop` controls whether arrow keys wrap around a list. Components that support `orientation` use it to choose the navigation axis.
819
+
820
+ A trigger's `shortcut="mod+k"` opens it from elsewhere on the page. A data menu runs item shortcuts while closed; a manually rendered MenuItem's shortcut only displays the keys. [Shortcuts](./utilities/shortcuts.md) covers registering your own.
821
+
822
+ ## Forms
823
+
824
+ Give a control a `name` to include its value in a form. This works for Select and Combobox as well as inputs and checkboxes.
825
+
826
+ ### Submit a Value
827
+
828
+ ```vue
829
+ <script setup lang="ts">
830
+ import { Checkbox } from "@ui-sdk/vue/checkbox"
831
+ </script>
832
+
833
+ <template>
834
+ <form action="/preferences" method="post">
835
+ <label
836
+ ><Checkbox name="updates" value="yes" default-checked /> Email me
837
+ updates</label
838
+ >
839
+ <button type="submit">Save</button>
840
+ <button type="reset">Reset</button>
841
+ </form>
842
+ </template>
843
+ ```
844
+
845
+ The checkbox submits `updates=yes` when checked. Reset restores its default value.
846
+
847
+ ### Reset a Controlled Value
848
+
849
+ A reset also goes through `@checked-change` and `v-model`. Keep the binding in place so your ref follows the reset:
850
+
851
+ ```vue
852
+ <script setup lang="ts">
853
+ import { Checkbox, type CheckedState } from "@ui-sdk/vue/checkbox"
854
+ import { ref } from "vue"
855
+
856
+ const checked = ref<CheckedState>(true)
857
+ const reason = ref("No change yet")
858
+ </script>
859
+
860
+ <template>
861
+ <form>
862
+ <label>
863
+ <Checkbox
864
+ v-model:checked="checked"
865
+ name="updates"
866
+ default-checked
867
+ @checked-change="(_next, details) => (reason = details.reason)"
868
+ />
869
+ Email me updates
870
+ </label>
871
+ <button type="reset">Reset</button>
872
+ <output>{{ reason }}</output>
873
+ </form>
874
+ </template>
875
+ ```
876
+
877
+ Uncheck the box, then press Reset. `default-checked` supplies the reset value. The event receives `true` with reason `"reset"`, and `v-model` updates `checked`. The event runs when reset requests a different value; it does not run if the checkbox is already at its default.
878
+
879
+ ### Label and Validate a Field
880
+
881
+ `Field` connects a control to its label, description, and error message:
882
+
883
+ ```vue
884
+ <script setup lang="ts">
885
+ import {
886
+ Field,
887
+ FieldDescription,
888
+ FieldError,
889
+ FieldLabel,
890
+ } from "@ui-sdk/vue/field"
891
+ import { Input } from "@ui-sdk/vue/input"
892
+ </script>
893
+
894
+ <template>
895
+ <Field name="email">
896
+ <FieldLabel>Email</FieldLabel>
897
+ <Input type="email" required />
898
+ <FieldDescription>We'll only use it for receipts.</FieldDescription>
899
+ <FieldError />
900
+ </Field>
901
+ </template>
902
+ ```
903
+
904
+ You can use a Textarea, Checkbox, Switch, Radio Group, or Select in place of the Input. [Field](./components/field.md) shows how they connect.
905
+
906
+ On controls that support them, `required`, `disabled`, `read-only`, and `form` follow native conventions. Disabled fields are left out of submission; read-only fields remain focusable and submitted.
907
+
908
+ ### Handle a Submission
909
+
910
+ On a normal submit, the browser checks constraints such as `required` and `type="email"` before running your handler.
911
+
912
+ 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:
913
+
914
+ ```vue
915
+ <script setup lang="ts">
916
+ import { Field, FieldError, FieldLabel } from "@ui-sdk/vue/field"
917
+ import { Input } from "@ui-sdk/vue/input"
918
+ import { ref } from "vue"
919
+
920
+ const error = ref("")
921
+
922
+ async function save(event: SubmitEvent) {
923
+ const body = new FormData(event.currentTarget as HTMLFormElement)
924
+ const response = await fetch("/api/preferences", { method: "POST", body })
925
+ error.value = response.ok ? "" : "That email is already taken."
926
+ }
927
+ </script>
928
+
929
+ <template>
930
+ <form @submit.prevent="save">
931
+ <Field name="email" :invalid="error ? true : undefined">
932
+ <FieldLabel>Email</FieldLabel>
933
+ <Input type="email" required @value-change="error = ''" />
934
+ <FieldError v-slot="{ state }">{{ error || state.message }}</FieldError>
935
+ </Field>
936
+ <button type="submit">Save</button>
937
+ </form>
938
+ </template>
939
+ ```
940
+
941
+ Without a handler, the form submits through its `action` URL.
942
+
943
+ ## Server Rendering
944
+
945
+ ui-sdk parts render HTML with Vue's server renderer. Their initial attributes include ARIA state and the group's tab stop. In Nuxt, pages and components render this way with no plugin or module.
946
+
947
+ ### Render the Initial State
948
+
949
+ ```vue
950
+ <script setup lang="ts">
951
+ import { Toggle } from "@ui-sdk/vue/toggle"
952
+ </script>
953
+
954
+ <template>
955
+ <Toggle default-pressed>Bold</Toggle>
956
+ </template>
957
+ ```
958
+
959
+ 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.
960
+
961
+ ### Read Browser Values After Mount
962
+
963
+ The server does not know the browser’s language. Start with the same placeholder on the server and client, then read the language after mount:
964
+
965
+ ```vue
966
+ <script setup lang="ts">
967
+ import { onMounted, ref } from "vue"
968
+
969
+ const language = ref("")
970
+ onMounted(() => {
971
+ language.value = navigator.language
972
+ })
973
+ </script>
974
+
975
+ <template>
976
+ <p>{{ language || "Loading language…" }}</p>
977
+ </template>
978
+ ```
979
+
980
+ Both initial renders show “Loading language…”. Once mounted, the paragraph shows the browser’s language. Read other browser-only values in the same place, or in an event handler.
981
+
982
+ ### Saved Preferences and Storage
983
+
984
+ 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.
985
+
986
+ #### Show Them Before Hydration
987
+
988
+ Render `Preferences` around your app. 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 Vue hydrates, so CSS that selects those attributes is right from the first paint:
989
+
990
+ ```vue title="app.vue"
991
+ <script setup lang="ts">
992
+ import { Preferences } from "@ui-sdk/vue/preferences"
993
+ import Page from "./page.vue"
994
+ import { colorScheme, skinTone } from "./preferences"
995
+
996
+ const preferences = [colorScheme, skinTone]
997
+ </script>
998
+
999
+ <template>
1000
+ <Preferences :preferences="preferences">
1001
+ <Page />
1002
+ </Preferences>
1003
+ </template>
1004
+ ```
1005
+
1006
+ Its element marks its attributes with `data-allow-mismatch="attribute"`, since the script changes them before hydration. A preference made with `global: true` goes on `<html>` instead. Pass `nonce` if your Content Security Policy only allows inline scripts with one.
1007
+
1008
+ Content that reads a preference through `usePreference` renders the default on the server and the saved value once mounted. Style by the attribute instead wherever the first paint has to be right, as [Style by Preferences](./core.md#style-by-preferences) shows.
1009
+
1010
+ #### Read a Cookie on the Server
1011
+
1012
+ 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 app as a prop, like `createSSRApp(App, { cookie })`. `setup` runs once per request, so create the preference there and share it with `provide`:
1013
+
1014
+ ```vue title="app.vue"
1015
+ <script setup lang="ts">
1016
+ import { createCookieStorage, createPreference } from "@ui-sdk/core"
1017
+ import { Preferences } from "@ui-sdk/vue/preferences"
1018
+ import { provide } from "vue"
1019
+ import Page from "./page.vue"
1020
+
1021
+ const props = defineProps<{ cookie: string | null }>()
1022
+ const skinTone = createPreference({
1023
+ values: ["default", "light", "medium", "dark"],
1024
+ defaultValue: "default",
1025
+ attribute: "data-skin-tone",
1026
+ storage: createCookieStorage("skin-tone", { cookie: props.cookie }),
1027
+ })
1028
+ provide("skin-tone", skinTone)
1029
+ </script>
1030
+
1031
+ <template>
1032
+ <Preferences :preferences="[skinTone]">
1033
+ <Page />
1034
+ </Preferences>
1035
+ </template>
1036
+ ```
1037
+
1038
+ The server reads the value from `cookie`, and the browser from `document.cookie`, so both render it and hydration matches.
1039
+
1040
+ Nuxt creates the app for you, so read the header with `useRequestHeader` in `app.vue` instead of taking a prop:
1041
+
1042
+ ```vue title="app.vue"
1043
+ <script setup lang="ts">
1044
+ import { createCookieStorage, createPreference } from "@ui-sdk/core"
1045
+ import { Preferences } from "@ui-sdk/vue/preferences"
1046
+
1047
+ const skinTone = createPreference({
1048
+ values: ["default", "light", "medium", "dark"],
1049
+ defaultValue: "default",
1050
+ attribute: "data-skin-tone",
1051
+ storage: createCookieStorage("skin-tone", {
1052
+ cookie: useRequestHeader("cookie"),
1053
+ }),
1054
+ })
1055
+ provide("skin-tone", skinTone)
1056
+ </script>
1057
+
1058
+ <template>
1059
+ <Preferences :preferences="[skinTone]">
1060
+ <NuxtPage />
1061
+ </Preferences>
1062
+ </template>
1063
+ ```
1064
+
1065
+ `useRequestHeader` returns the whole `Cookie` header on the server and undefined in the browser, where the storage reads `document.cookie` instead. `useCookie` doesn't fit here, since it reads one cookie by name and the storage takes the header.
1066
+
1067
+ #### Render the Storage Script Yourself
1068
+
1069
+ `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. Vue templates leave `<script>` tags out, so render it from a render function:
1070
+
1071
+ ```ts title="toolbar.ts"
1072
+ import { createLocalStorage } from "@ui-sdk/core"
1073
+ import { defineComponent, h, onMounted, shallowRef } from "vue"
1074
+
1075
+ const density = createLocalStorage("density")
1076
+ const script = density.script({
1077
+ attribute: "data-density",
1078
+ values: ["comfortable", "compact"],
1079
+ })
1080
+
1081
+ const Toolbar = defineComponent((_props, { slots }) => {
1082
+ const value = shallowRef("comfortable")
1083
+ // The script is in the server's HTML until then.
1084
+ const mounted = shallowRef(false)
1085
+ onMounted(() => {
1086
+ value.value = density.get() === "compact" ? "compact" : "comfortable"
1087
+ mounted.value = true
1088
+ })
1089
+ return () => [
1090
+ h(
1091
+ "div",
1092
+ { "data-density": value.value, "data-allow-mismatch": "attribute" },
1093
+ slots.default?.()
1094
+ ),
1095
+ mounted.value || !script ? null : h("script", { innerHTML: script }),
1096
+ ]
1097
+ })
1098
+
1099
+ export { Toolbar }
1100
+ ```
1101
+
1102
+ - **Only from the server.** The browser never runs a script Vue inserts, so drop it once mounted.
1103
+ - **The element differs until hydration.** The script changes its attribute before Vue hydrates, so mark it with `data-allow-mismatch="attribute"`. Once mounted, render the saved value from state, as above.
1104
+ - **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.
1105
+ - **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`.
1106
+ - **Not for cookies.** A cookie storage's `script` returns undefined, since the server reads the cookie and renders the value itself.
1107
+
1108
+ To run before the page's content at all, render `preferencesScript([...])` from `@ui-sdk/core` in `<head>` yourself, wherever your server writes it. Only global preferences make sense there, since the others go on the script's parent element.
1109
+
1110
+ ### Keep IDs and Values Consistent
1111
+
1112
+ Use the same initial component values on the server and client. For example, do not render an open dialog on the server and a closed one for the first client render.
1113
+
1114
+ Parts generate IDs for relationships such as a Dialog trigger’s `aria-controls` and a Field label’s `for`. You usually do not need to supply them.
1115
+
1116
+ 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.
1117
+
1118
+ Use classes or data attributes for styling, rather than generated IDs.
1119
+
1120
+ Popups render in place, so your server layout doesn't need a portal root. [Popups](./popups.md) covers containers; [Forms](./forms.md) covers submission and validation.
1121
+
1122
+ ## Rendering Primitives
1123
+
1124
+ 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 Vue.
1125
+
1126
+ ### Create the Parts
1127
+
1128
+ `createParts` maps slots to elements. Save this beside `rating.ts`:
1129
+
1130
+ ```ts title="rating-parts.ts"
1131
+ import { createParts } from "@ui-sdk/vue/primitive"
1132
+ import { rating } from "./rating"
1133
+
1134
+ const { Root: RatingRoot, Star: RatingStar } = createParts(rating, {
1135
+ root: "div",
1136
+ star: "button",
1137
+ })
1138
+
1139
+ export { RatingRoot, RatingStar }
1140
+ ```
1141
+
1142
+ `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`.
1143
+
1144
+ If you leave out the `root` tag, `Root` still shares the instance but renders only its children.
1145
+
1146
+ ### Render the Rating
1147
+
1148
+ 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:
1149
+
1150
+ ```vue
1151
+ <script setup lang="ts">
1152
+ import { RatingRoot, RatingStar } from "./rating-parts"
1153
+ </script>
1154
+
1155
+ <template>
1156
+ <RatingRoot :default-value="4" aria-label="Rating" class="flex gap-1">
1157
+ <RatingStar
1158
+ v-for="star in 5"
1159
+ :key="star"
1160
+ :value="String(star)"
1161
+ :aria-label="`${star} ${star === 1 ? 'star' : 'stars'}`"
1162
+ class="opacity-40 data-filled:opacity-100"
1163
+ >★</RatingStar
1164
+ >
1165
+ </RatingRoot>
1166
+ </template>
1167
+ ```
1168
+
1169
+ 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.
1170
+
1171
+ Generated parts also support the same default and `#render` slots as the components in the catalog. Use [Parts and Composition](./parts-and-composition.md) when you want to change their markup.
1172
+
1173
+ ### Add Logic to a Part
1174
+
1175
+ 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.
1176
+
1177
+ ## Custom Parts
1178
+
1179
+ 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).
1180
+
1181
+ ### Write a Root Part
1182
+
1183
+ Here's a replacement for the generated `RatingRoot`. `usePrimitive` creates its instance, and `usePart` connects the `root` slot:
1184
+
1185
+ ```ts title="rating-root.ts"
1186
+ import { usePart, usePrimitive, type PartSlots } from "@ui-sdk/vue/primitive"
1187
+ import { defineComponent, type HTMLAttributes } from "vue"
1188
+ import { rating } from "./rating"
1189
+
1190
+ const RatingRoot = defineComponent(
1191
+ (
1192
+ props: { value?: number; defaultValue?: number } & HTMLAttributes,
1193
+ { emit }
1194
+ ) => {
1195
+ const handle = usePrimitive(rating, () => ({
1196
+ value: props.value,
1197
+ defaultValue: props.defaultValue,
1198
+ onValueChange(value, details) {
1199
+ emit("value-change", value, details)
1200
+ if (!details.canceled) {
1201
+ emit("update:value", value)
1202
+ }
1203
+ },
1204
+ }))
1205
+ return usePart(handle, "root", undefined, undefined, "div")
1206
+ },
1207
+ {
1208
+ inheritAttrs: false,
1209
+ props: ["value", "defaultValue"],
1210
+ emits: ["value-change", "update:value"],
1211
+ slots: Object as PartSlots<typeof rating, "root">,
1212
+ }
1213
+ )
1214
+
1215
+ export { RatingRoot }
1216
+ ```
1217
+
1218
+ Call both helpers in `setup`. The options getter keeps values current; `usePrimitive` also shares the instance with child parts. `usePart` returns a render function that forwards attrs and slots. `inheritAttrs: false` avoids applying attrs twice.
1219
+
1220
+ `HTMLAttributes` makes native attrs available to TypeScript callers; only `value` and `defaultValue` are declared component props, so the other attrs still reach `usePart`.
1221
+
1222
+ This root supports `v-model:value` by emitting `update:value` after the change callback has had a chance to cancel. `PartSlots` types the default and `#render` slots. The adapter manages the instance's lifetime.
1223
+
1224
+ ### Connect a Child Part
1225
+
1226
+ Pass the primitive itself to `usePart` to find the instance shared by the root above it. This `RatingStar` works inside the manual root above or the generated `RatingRoot`:
1227
+
1228
+ ```ts title="rating-star.ts"
1229
+ import { usePart, type PartSlots } from "@ui-sdk/vue/primitive"
1230
+ import { defineComponent, type ButtonHTMLAttributes } from "vue"
1231
+ import { rating } from "./rating"
1232
+
1233
+ const RatingStar = defineComponent(
1234
+ (props: { value: string } & Omit<ButtonHTMLAttributes, "value">) => {
1235
+ return usePart(
1236
+ rating,
1237
+ "star",
1238
+ () => ({ value: props.value }),
1239
+ undefined,
1240
+ "button"
1241
+ )
1242
+ },
1243
+ {
1244
+ inheritAttrs: false,
1245
+ props: ["value"],
1246
+ slots: Object as PartSlots<typeof rating, "star">,
1247
+ }
1248
+ )
1249
+
1250
+ export { RatingStar }
1251
+ ```
1252
+
1253
+ Use it in place of the generated star in [Render the Rating](./rendering.md#render-the-rating). `ButtonHTMLAttributes` types its native attrs. The `value` getter identifies the star; the remaining props go to its button. Keep it inside `RatingRoot` and give it an `aria-label`.
1254
+
1255
+ ### Merge Props
1256
+
1257
+ `usePart` merges the component's attrs with the slot's. Slot attributes win, classes and styles combine, and your listeners run first. An `id`, or a name or typing hint you pass, like `aria-label` or `autocomplete`, replaces the slot's. Call `preventDefault()` in a listener to skip the built-in handler.
1258
+
1259
+ The last argument can be a tag, or an object with `tag`, `children`, and `extend`. Use `extend` for extra props after the slot's props, such as attributes supplied by a Field. Keep the slot's roles, IDs, and ARIA state intact.
1260
+
1261
+ ### Compose a Popup
1262
+
1263
+ A custom component can also use an existing Popover or Dialog. [Compose Ours](./core.md#compose-ours) shows that connection with an emoji picker.
1264
+
1265
+ ## Components
1266
+
1267
+ Each page has the component's composition, examples, styling hooks and every prop.
1268
+
1269
+ - [Accordion](./components/accordion.md): Show and hide sections of content from their headings. Import from `@ui-sdk/vue/accordion`.
1270
+ - [Alert Dialog](./components/alert-dialog.md): Ask someone to confirm an action before you continue. Import from `@ui-sdk/vue/alert-dialog`.
1271
+ - [Autocomplete](./components/autocomplete.md): Suggest text while someone types, without limiting what they can enter. Import from `@ui-sdk/vue/autocomplete`.
1272
+ - [Avatar](./components/avatar.md): Show an image with a fallback while it loads or if it fails. Import from `@ui-sdk/vue/avatar`.
1273
+ - [Button](./components/button.md): Run an action, show that it's working, and keep it reachable when disabled. Import from `@ui-sdk/vue/button`.
1274
+ - [Calendar](./components/calendar.md): Choose one date, several dates, or a range from a month grid. Import from `@ui-sdk/vue/calendar`.
1275
+ - [Carousel](./components/carousel.md): Page through slides with native scrolling, keyboard controls, and optional rotation. Import from `@ui-sdk/vue/carousel`.
1276
+ - [Checkbox](./components/checkbox.md): Choose one or more options, individually or as a group. Import from `@ui-sdk/vue/checkbox`.
1277
+ - [Collapsible](./components/collapsible.md): Show and hide a panel, preserve its contents, and animate its height. Import from `@ui-sdk/vue/collapsible`.
1278
+ - [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/vue/color-picker`.
1279
+ - [Combobox](./components/combobox.md): Search a list of options and choose one or several values. Import from `@ui-sdk/vue/combobox`.
1280
+ - [Command](./components/command.md): Search commands, run actions, and open pages of more choices. Import from `@ui-sdk/vue/command`.
1281
+ - [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/vue/date-input`.
1282
+ - [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/vue/date-picker`.
1283
+ - [Dialog](./components/dialog.md): Open a focused task over the page. Import from `@ui-sdk/vue/dialog`.
1284
+ - [Drawer](./components/drawer.md): A sheet you can open, drag, and resize from an edge of the screen. Import from `@ui-sdk/vue/drawer`.
1285
+ - [Field](./components/field.md): Connect a form control to its label, description, and validation errors. Import from `@ui-sdk/vue/field`.
1286
+ - [File Input](./components/file-input.md): Choose, drop, or paste files, validate them, and submit them with a form. Import from `@ui-sdk/vue/file-input`.
1287
+ - [Hover Card](./components/hover-card.md): Preview a link on hover or keyboard focus while keeping its normal navigation. Import from `@ui-sdk/vue/hover-card`.
1288
+ - [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/vue/image-cropper`.
1289
+ - [Input](./components/input.md): Enter text, add a clear button, and connect the value to your form or state. Import from `@ui-sdk/vue/input`.
1290
+ - [Label](./components/label.md): Name any control with a label, and let a press on the label reach it. Import from `@ui-sdk/vue/label`.
1291
+ - [Listbox](./components/listbox.md): Choose one or several options from an always-visible list. Import from `@ui-sdk/vue/listbox`.
1292
+ - [Menu](./components/menu.md): Choose actions and settings from a button or a context menu. Import from `@ui-sdk/vue/menu`.
1293
+ - [Menubar](./components/menubar.md): Move between an application’s menus with one tab stop. Import from `@ui-sdk/vue/menubar`.
1294
+ - [Meter](./components/meter.md): Show a measurement within a known range, such as storage used or an account quota. Import from `@ui-sdk/vue/meter`.
1295
+ - [Navigation Menu](./components/navigation-menu.md): Groups site links into panels that open from a row of triggers. Import from `@ui-sdk/vue/navigation-menu`.
1296
+ - [Number Input](./components/number-input.md): Enter numbers, step through values, and format them for a locale. Import from `@ui-sdk/vue/number-input`.
1297
+ - [OTP Input](./components/otp-input.md): Enter a one-time code with support for pasting, autofill, and native form validation. Import from `@ui-sdk/vue/otp-input`.
1298
+ - [Pagination](./components/pagination.md): Navigate pages with buttons, links, or your router. Import from `@ui-sdk/vue/pagination`.
1299
+ - [Password Input](./components/password-input.md): Enter a password, show or hide it, and use it in a native form. Import from `@ui-sdk/vue/password-input`.
1300
+ - [Popover](./components/popover.md): Edit or explore something beside its trigger. Import from `@ui-sdk/vue/popover`.
1301
+ - [Preferences](./components/preferences.md): Apply user preferences to data attributes so your styles can follow them. Import from `@ui-sdk/vue/preferences`.
1302
+ - [Progress](./components/progress.md): Show a task’s progress, or an indeterminate state while its progress is unknown. Import from `@ui-sdk/vue/progress`.
1303
+ - [Radio](./components/radio.md): Choose one option, move the selection with arrow keys, and submit it with a form. Import from `@ui-sdk/vue/radio`.
1304
+ - [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/vue/rating`.
1305
+ - [Resizable](./components/resizable.md): Resize, collapse, and save panel layouts with pointer and keyboard controls. Import from `@ui-sdk/vue/resizable`.
1306
+ - [Scroll Area](./components/scroll-area.md): Add styled scrollbars, edge indicators, and follow-to-end behavior to native scrolling. Import from `@ui-sdk/vue/scroll-area`.
1307
+ - [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/vue/segmented-control`.
1308
+ - [Select](./components/select.md): Choose one value or several from a popup list. Import from `@ui-sdk/vue/select`.
1309
+ - [Separator](./components/separator.md): Divide groups of content with a horizontal or vertical line. Import from `@ui-sdk/vue/separator`.
1310
+ - [Slider](./components/slider.md): Adjust a number or a range with draggable thumbs and keyboard controls. Import from `@ui-sdk/vue/slider`.
1311
+ - [Switch](./components/switch.md): Turn a setting on or off. Import from `@ui-sdk/vue/switch`.
1312
+ - [Tabs](./components/tabs.md): Switch between related panels with one tab selected at a time. Import from `@ui-sdk/vue/tabs`.
1313
+ - [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/vue/textarea`.
1314
+ - [Time Input](./components/time-input.md): Type a time one part at a time, on the clock your user's language uses.
1315
+ - [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/vue/timeline`.
1316
+ - [Toast](./components/toast.md): Show notifications with actions, timed dismissal, and screen reader announcements. Import from `@ui-sdk/vue/toast`.
1317
+ - [Toggle](./components/toggle.md): Keep a button pressed, or group toggles with a shared value and arrow-key navigation. Import from `@ui-sdk/vue/toggle`.
1318
+ - [Token Input](./components/token-input.md): Mix editable text with tokens for files, people, tags, and other references. Import from `@ui-sdk/vue/token-input`.
1319
+ - [Toolbar](./components/toolbar.md): Give buttons, toggles, menus and fields one tab stop with arrow-key navigation. Import from `@ui-sdk/vue/toolbar`.
1320
+ - [Tooltip](./components/tooltip.md): Name a control or add a short hint on hover and keyboard focus. Import from `@ui-sdk/vue/tooltip`.
1321
+ - [Tree](./components/tree.md): Browse a hierarchy of files, pages, or choices with expandable branches. Import from `@ui-sdk/vue/tree`.
1322
+ - [Trigger Menu](./components/trigger-menu.md): "Opens suggestions at the caret when you type a trigger such as @, /, or #." Import from `@ui-sdk/vue/trigger-menu`.
1323
+ - [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/vue/waterfall`.
1324
+
1325
+ ## Utilities
1326
+
1327
+ ### Async Options
1328
+
1329
+ `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.
1330
+
1331
+ Change the movie search and wait for the results. Use the arrow keys to choose a movie, then press Enter.
1332
+
1333
+ #### Load Results
1334
+
1335
+ Pass a loader to the helper. This example expects `/api/movies` to return an array of movie titles:
1336
+
1337
+ ```vue
1338
+ <script setup lang="ts">
1339
+ import { useAsyncOptions } from "@ui-sdk/vue/async-options"
1340
+ import { Combobox, ComboboxContent, ComboboxInput } from "@ui-sdk/vue/combobox"
1341
+
1342
+ const movies = useAsyncOptions<string>(async (query, { signal }) => {
1343
+ const response = await fetch(`/api/movies?q=${encodeURIComponent(query)}`, {
1344
+ signal,
1345
+ })
1346
+ if (!response.ok) throw new Error("Couldn't load movies")
1347
+ return response.json()
1348
+ })
1349
+ const { state } = movies
1350
+ </script>
1351
+
1352
+ <template>
1353
+ <Combobox
1354
+ :items="state.items"
1355
+ :filter="false"
1356
+ :status="state.status"
1357
+ @query-change="movies.setQuery"
1358
+ >
1359
+ <ComboboxInput aria-label="Movie" placeholder="Search movies…" />
1360
+ <ComboboxContent />
1361
+ </Combobox>
1362
+ </template>
1363
+ ```
1364
+
1365
+ `state` is a ref, unwrapped in the template. In script, read `state.value.items` or `state.value.status`.
1366
+
1367
+ Pass `signal` to `fetch` so a newer search can cancel the request. The helper cancels on unmount. `filter` set to false leaves matching and ordering to the server.
1368
+
1369
+ Use `ComboboxStatus` and `ComboboxEmpty` to show loading, error, and empty messages, as in the demo. The same pattern works with [Select](./components/select.md#async-search), [Autocomplete](./components/autocomplete.md#async-suggestions), and [Command](./components/command.md#async-results).
1370
+
1371
+ #### Choose When to Load
1372
+
1373
+ The second argument sets the starting behavior:
1374
+
1375
+ | Option | What It Does | Default |
1376
+ | ----------- | ------------------------------------------------------- | ------- |
1377
+ | `debounce` | Milliseconds typing must pause before a request starts. | `200` |
1378
+ | `query` | The starting search text. | `""` |
1379
+ | `immediate` | Loads that text as soon as the component mounts. | `false` |
1380
+
1381
+ For initial choices, add `{ immediate: true, debounce: 300 }` after the loader. 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 `default-query`.
1382
+
1383
+ #### Load More
1384
+
1385
+ For paged results, the loader receives `page`, starting at `0`. Return `{ items, hasMore }`:
1386
+
1387
+ ```ts
1388
+ import type { AsyncOptionsContext } from "@ui-sdk/vue/async-options"
1389
+
1390
+ async function loadMoviePages(
1391
+ query: string,
1392
+ { signal, page }: AsyncOptionsContext
1393
+ ) {
1394
+ const response = await fetch(
1395
+ `/api/movies?q=${encodeURIComponent(query)}&page=${page}`,
1396
+ { signal }
1397
+ )
1398
+ if (!response.ok) throw new Error("Couldn't load movies")
1399
+ const data: { titles: string[]; next: number | null } = await response.json()
1400
+ return { items: data.titles, hasMore: data.next !== null }
1401
+ }
1402
+
1403
+ export { loadMoviePages }
1404
+ ```
1405
+
1406
+ Pass `loadMoviePages` to `useAsyncOptions` in place of the inline loader above.
1407
+
1408
+ Connect `@load-more="movies.loadMore"` on Combobox. It asks for another page when scrolling or keyboard navigation reaches the last item. The helper appends the new items.
1409
+
1410
+ Open the movie list and scroll down, or use Arrow Down to reach the last result. The next page is added to the list.
1411
+
1412
+ #### Retry a Search
1413
+
1414
+ `status` is `"loading"` during a request, `"error"` after a failure, and `"idle"` otherwise. `error` holds the failure, and the previous items stay available.
1415
+
1416
+ Call `movies.reload()` from a retry button to load the current query again, starting at page `0`. `state.value.hasMore` tells you whether another page is available.
1417
+
1418
+ ### Shortcuts
1419
+
1420
+ `useShortcut` registers a shortcut while your component is mounted. For a component that already accepts `shortcut`, such as a dialog trigger, use that prop directly.
1421
+
1422
+ Press N to create a note. Press Command+S on Mac or Control+S elsewhere to save. The buttons run the same actions.
1423
+
1424
+ #### Register a Shortcut
1425
+
1426
+ Keys are joined with `+`. `mod` means Command on Apple platforms and Control elsewhere. This example also displays the keys using `useShortcutHint`:
1427
+
1428
+ ```vue
1429
+ <script setup lang="ts">
1430
+ import { useShortcut, useShortcutHint } from "@ui-sdk/vue/shortcuts"
1431
+ import { ref } from "vue"
1432
+
1433
+ const count = ref(0)
1434
+ const addNote = () => count.value++
1435
+ useShortcut("mod+enter", addNote, { description: "New note" })
1436
+ const { text, ariaKeyshortcuts } = useShortcutHint("mod+enter")
1437
+ </script>
1438
+
1439
+ <template>
1440
+ <button @click="addNote" :aria-keyshortcuts="ariaKeyshortcuts">
1441
+ New note ({{ count }}) <kbd aria-hidden="true">{{ text }}</kbd>
1442
+ </button>
1443
+ </template>
1444
+ ```
1445
+
1446
+ Call the helper in setup. It unregisters when the component goes away. Keys and options can be getters; return `null` for keys to leave the shortcut unregistered.
1447
+
1448
+ #### Show the Keys
1449
+
1450
+ `useShortcutHint` supplies display text and `aria-keyshortcuts`. It keeps the server and first client render consistent, then updates for the user's platform after mount.
1451
+
1452
+ Its fields are computed refs. The example destructures them so the template unwraps them automatically.
1453
+
1454
+ Outside rendering, use `formatShortcut` for display text or `ariaKeyshortcuts` for the ARIA value, from the same entry.
1455
+
1456
+ #### While Someone Is Typing
1457
+
1458
+ Shortcuts with Command, Control, or Option can run in text fields. Plain letters and other keys stay available for typing. Set `allowInInput: true` when a shortcut should work there too, such as Escape to clear a search.
1459
+
1460
+ #### Limit a Shortcut to an Area
1461
+
1462
+ Use a named `scope` for an editor or another area. Turn it on while that area has focus:
1463
+
1464
+ ```vue
1465
+ <script setup lang="ts">
1466
+ import { shortcuts, useShortcut } from "@ui-sdk/vue/shortcuts"
1467
+ import { onScopeDispose, ref } from "vue"
1468
+
1469
+ const text = ref("")
1470
+ useShortcut(
1471
+ "escape",
1472
+ () => {
1473
+ text.value = ""
1474
+ },
1475
+ { scope: "note", allowInInput: true }
1476
+ )
1477
+ onScopeDispose(() => shortcuts.setScope("note", false))
1478
+ </script>
1479
+
1480
+ <template>
1481
+ <textarea
1482
+ aria-label="Note"
1483
+ v-model="text"
1484
+ @focus="shortcuts.setScope('note', true)"
1485
+ @blur="shortcuts.setScope('note', false)"
1486
+ />
1487
+ </template>
1488
+ ```
1489
+
1490
+ Scope names are shared, so choose one for each area you want to enable together. A `when` function can add another condition, checked each time the keys are pressed.
1491
+
1492
+ Focus the note and type some text. Press Command+Enter on Mac or Control+Enter elsewhere to save, or Escape to clear it. Move focus outside the note to turn off its shortcuts.
1493
+
1494
+ #### Inside a Popup
1495
+
1496
+ Page shortcuts pause while a modal is open. Pass `layer`, an element inside the popup or a getter that returns one, to make a shortcut belong to that popup. It runs only while that popup is open.
1497
+
1498
+ When shortcuts share the same keys, the topmost open layer wins, then the most recently registered shortcut.
1499
+
1500
+ #### List Available Shortcuts
1501
+
1502
+ Call `shortcuts.list()` when a help screen opens. It returns the shortcuts that can run now, including their `keys`, formatted `hint`, and `description`. Add descriptions when registering shortcuts so the help screen can explain each action.