@mortenbrudvik/waveui 0.9.0 → 0.11.0

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 (264) hide show
  1. package/README.md +758 -152
  2. package/dist/components/button/Toolbar.cjs +1 -1
  3. package/dist/components/button/Toolbar.cjs.map +1 -1
  4. package/dist/components/button/Toolbar.mjs +25 -30
  5. package/dist/components/button/Toolbar.mjs.map +1 -1
  6. package/dist/components/button/Toolbar.navigation.cjs +2 -0
  7. package/dist/components/button/Toolbar.navigation.cjs.map +1 -0
  8. package/dist/components/button/Toolbar.navigation.mjs +17 -0
  9. package/dist/components/button/Toolbar.navigation.mjs.map +1 -0
  10. package/dist/components/data-display/Avatar.cjs +1 -1
  11. package/dist/components/data-display/Avatar.cjs.map +1 -1
  12. package/dist/components/data-display/Avatar.context.cjs +2 -0
  13. package/dist/components/data-display/Avatar.context.cjs.map +1 -0
  14. package/dist/components/data-display/Avatar.context.mjs +9 -0
  15. package/dist/components/data-display/Avatar.context.mjs.map +1 -0
  16. package/dist/components/data-display/Avatar.mjs +66 -61
  17. package/dist/components/data-display/Avatar.mjs.map +1 -1
  18. package/dist/components/data-display/InteractionTag.cjs +2 -0
  19. package/dist/components/data-display/InteractionTag.cjs.map +1 -0
  20. package/dist/components/data-display/InteractionTag.mjs +234 -0
  21. package/dist/components/data-display/InteractionTag.mjs.map +1 -0
  22. package/dist/components/data-display/Tag.cjs +1 -1
  23. package/dist/components/data-display/Tag.cjs.map +1 -1
  24. package/dist/components/data-display/Tag.mjs +222 -90
  25. package/dist/components/data-display/Tag.mjs.map +1 -1
  26. package/dist/components/data-display/TagGroup.cjs +2 -0
  27. package/dist/components/data-display/TagGroup.cjs.map +1 -0
  28. package/dist/components/data-display/TagGroup.context.cjs +2 -0
  29. package/dist/components/data-display/TagGroup.context.cjs.map +1 -0
  30. package/dist/components/data-display/TagGroup.context.mjs +113 -0
  31. package/dist/components/data-display/TagGroup.context.mjs.map +1 -0
  32. package/dist/components/data-display/TagGroup.mjs +149 -0
  33. package/dist/components/data-display/TagGroup.mjs.map +1 -0
  34. package/dist/components/data-display/tagStyles.cjs +2 -0
  35. package/dist/components/data-display/tagStyles.cjs.map +1 -0
  36. package/dist/components/data-display/tagStyles.mjs +108 -0
  37. package/dist/components/data-display/tagStyles.mjs.map +1 -0
  38. package/dist/components/feedback/Toaster.cjs +1 -1
  39. package/dist/components/feedback/Toaster.cjs.map +1 -1
  40. package/dist/components/feedback/Toaster.mjs +213 -181
  41. package/dist/components/feedback/Toaster.mjs.map +1 -1
  42. package/dist/components/input/Calendar.cjs +2 -0
  43. package/dist/components/input/Calendar.cjs.map +1 -0
  44. package/dist/components/input/Calendar.mjs +352 -0
  45. package/dist/components/input/Calendar.mjs.map +1 -0
  46. package/dist/components/input/Calendar.views.cjs +2 -0
  47. package/dist/components/input/Calendar.views.cjs.map +1 -0
  48. package/dist/components/input/Calendar.views.mjs +194 -0
  49. package/dist/components/input/Calendar.views.mjs.map +1 -0
  50. package/dist/components/input/ColorArea.cjs +1 -1
  51. package/dist/components/input/ColorArea.cjs.map +1 -1
  52. package/dist/components/input/ColorArea.mjs +18 -18
  53. package/dist/components/input/ColorArea.mjs.map +1 -1
  54. package/dist/components/input/Combobox.cjs +1 -1
  55. package/dist/components/input/Combobox.cjs.map +1 -1
  56. package/dist/components/input/Combobox.expand.cjs +1 -1
  57. package/dist/components/input/Combobox.expand.cjs.map +1 -1
  58. package/dist/components/input/Combobox.expand.mjs +7 -7
  59. package/dist/components/input/Combobox.expand.mjs.map +1 -1
  60. package/dist/components/input/Combobox.mjs +232 -156
  61. package/dist/components/input/Combobox.mjs.map +1 -1
  62. package/dist/components/input/Combobox.text.cjs +2 -0
  63. package/dist/components/input/Combobox.text.cjs.map +1 -0
  64. package/dist/components/input/Combobox.text.mjs +24 -0
  65. package/dist/components/input/Combobox.text.mjs.map +1 -0
  66. package/dist/components/input/DatePicker.cjs +1 -1
  67. package/dist/components/input/DatePicker.cjs.map +1 -1
  68. package/dist/components/input/DatePicker.mjs +203 -288
  69. package/dist/components/input/DatePicker.mjs.map +1 -1
  70. package/dist/components/input/Dropdown.cjs +1 -1
  71. package/dist/components/input/Dropdown.cjs.map +1 -1
  72. package/dist/components/input/Dropdown.mjs +135 -109
  73. package/dist/components/input/Dropdown.mjs.map +1 -1
  74. package/dist/components/input/Listbox.cjs +2 -0
  75. package/dist/components/input/Listbox.cjs.map +1 -0
  76. package/dist/components/input/Listbox.mjs +134 -0
  77. package/dist/components/input/Listbox.mjs.map +1 -0
  78. package/dist/components/input/Option.cjs +1 -1
  79. package/dist/components/input/Option.cjs.map +1 -1
  80. package/dist/components/input/Option.mjs +163 -97
  81. package/dist/components/input/Option.mjs.map +1 -1
  82. package/dist/components/input/SwatchPicker.cjs +1 -1
  83. package/dist/components/input/SwatchPicker.cjs.map +1 -1
  84. package/dist/components/input/SwatchPicker.context.cjs +2 -0
  85. package/dist/components/input/SwatchPicker.context.cjs.map +1 -0
  86. package/dist/components/input/SwatchPicker.context.mjs +35 -0
  87. package/dist/components/input/SwatchPicker.context.mjs.map +1 -0
  88. package/dist/components/input/SwatchPicker.mjs +131 -103
  89. package/dist/components/input/SwatchPicker.mjs.map +1 -1
  90. package/dist/components/input/SwatchPicker.swatches.cjs +2 -0
  91. package/dist/components/input/SwatchPicker.swatches.cjs.map +1 -0
  92. package/dist/components/input/SwatchPicker.swatches.mjs +207 -0
  93. package/dist/components/input/SwatchPicker.swatches.mjs.map +1 -0
  94. package/dist/components/input/TagPicker.cjs +1 -1
  95. package/dist/components/input/TagPicker.cjs.map +1 -1
  96. package/dist/components/input/TagPicker.context.cjs +2 -0
  97. package/dist/components/input/TagPicker.context.cjs.map +1 -0
  98. package/dist/components/input/TagPicker.context.mjs +20 -0
  99. package/dist/components/input/TagPicker.context.mjs.map +1 -0
  100. package/dist/components/input/TagPicker.create.cjs +2 -0
  101. package/dist/components/input/TagPicker.create.cjs.map +1 -0
  102. package/dist/components/input/TagPicker.create.mjs +24 -0
  103. package/dist/components/input/TagPicker.create.mjs.map +1 -0
  104. package/dist/components/input/TagPicker.mjs +13 -274
  105. package/dist/components/input/TagPicker.mjs.map +1 -1
  106. package/dist/components/input/TagPicker.option.cjs +2 -0
  107. package/dist/components/input/TagPicker.option.cjs.map +1 -0
  108. package/dist/components/input/TagPicker.option.mjs +45 -0
  109. package/dist/components/input/TagPicker.option.mjs.map +1 -0
  110. package/dist/components/input/TagPicker.overflow.cjs +2 -0
  111. package/dist/components/input/TagPicker.overflow.cjs.map +1 -0
  112. package/dist/components/input/TagPicker.overflow.mjs +153 -0
  113. package/dist/components/input/TagPicker.overflow.mjs.map +1 -0
  114. package/dist/components/input/TagPicker.parts.cjs +2 -0
  115. package/dist/components/input/TagPicker.parts.cjs.map +1 -0
  116. package/dist/components/input/TagPicker.parts.mjs +425 -0
  117. package/dist/components/input/TagPicker.parts.mjs.map +1 -0
  118. package/dist/components/input/TagPicker.root.cjs +2 -0
  119. package/dist/components/input/TagPicker.root.cjs.map +1 -0
  120. package/dist/components/input/TagPicker.root.mjs +416 -0
  121. package/dist/components/input/TagPicker.root.mjs.map +1 -0
  122. package/dist/components/input/TimePicker.cjs +1 -1
  123. package/dist/components/input/TimePicker.cjs.map +1 -1
  124. package/dist/components/input/TimePicker.mjs +77 -77
  125. package/dist/components/input/TimePicker.mjs.map +1 -1
  126. package/dist/components/input/dateUtils.cjs +1 -1
  127. package/dist/components/input/dateUtils.cjs.map +1 -1
  128. package/dist/components/input/dateUtils.mjs +114 -96
  129. package/dist/components/input/dateUtils.mjs.map +1 -1
  130. package/dist/components/input/pickerLabels.cjs +2 -0
  131. package/dist/components/input/pickerLabels.cjs.map +1 -0
  132. package/dist/components/input/pickerLabels.mjs +16 -0
  133. package/dist/components/input/pickerLabels.mjs.map +1 -0
  134. package/dist/components/input/pickerValues.cjs +2 -0
  135. package/dist/components/input/pickerValues.cjs.map +1 -0
  136. package/dist/components/input/pickerValues.mjs +24 -0
  137. package/dist/components/input/pickerValues.mjs.map +1 -0
  138. package/dist/components/layout/Accordion.cjs.map +1 -1
  139. package/dist/components/layout/Accordion.mjs.map +1 -1
  140. package/dist/components/navigation/Hamburger.cjs +2 -0
  141. package/dist/components/navigation/Hamburger.cjs.map +1 -0
  142. package/dist/components/navigation/Hamburger.mjs +54 -0
  143. package/dist/components/navigation/Hamburger.mjs.map +1 -0
  144. package/dist/components/navigation/Nav.cjs +1 -1
  145. package/dist/components/navigation/Nav.cjs.map +1 -1
  146. package/dist/components/navigation/Nav.context.cjs +2 -0
  147. package/dist/components/navigation/Nav.context.cjs.map +1 -0
  148. package/dist/components/navigation/Nav.context.mjs +137 -0
  149. package/dist/components/navigation/Nav.context.mjs.map +1 -0
  150. package/dist/components/navigation/Nav.mjs +151 -258
  151. package/dist/components/navigation/Nav.mjs.map +1 -1
  152. package/dist/components/navigation/Nav.parts.cjs +2 -0
  153. package/dist/components/navigation/Nav.parts.cjs.map +1 -0
  154. package/dist/components/navigation/Nav.parts.mjs +77 -0
  155. package/dist/components/navigation/Nav.parts.mjs.map +1 -0
  156. package/dist/components/navigation/Nav.root.cjs +2 -0
  157. package/dist/components/navigation/Nav.root.cjs.map +1 -0
  158. package/dist/components/navigation/Nav.root.mjs +78 -0
  159. package/dist/components/navigation/Nav.root.mjs.map +1 -0
  160. package/dist/components/navigation/NavDrawer.cjs +2 -0
  161. package/dist/components/navigation/NavDrawer.cjs.map +1 -0
  162. package/dist/components/navigation/NavDrawer.context.cjs +2 -0
  163. package/dist/components/navigation/NavDrawer.context.cjs.map +1 -0
  164. package/dist/components/navigation/NavDrawer.context.mjs +11 -0
  165. package/dist/components/navigation/NavDrawer.context.mjs.map +1 -0
  166. package/dist/components/navigation/NavDrawer.mjs +179 -0
  167. package/dist/components/navigation/NavDrawer.mjs.map +1 -0
  168. package/dist/components/overlays/Dialog.cjs +1 -1
  169. package/dist/components/overlays/Dialog.cjs.map +1 -1
  170. package/dist/components/overlays/Dialog.mjs +190 -125
  171. package/dist/components/overlays/Dialog.mjs.map +1 -1
  172. package/dist/components/overlays/Dialog.shared.cjs +1 -1
  173. package/dist/components/overlays/Dialog.shared.cjs.map +1 -1
  174. package/dist/components/overlays/Dialog.shared.mjs +157 -81
  175. package/dist/components/overlays/Dialog.shared.mjs.map +1 -1
  176. package/dist/components/overlays/Dialog.slots.cjs +2 -0
  177. package/dist/components/overlays/Dialog.slots.cjs.map +1 -0
  178. package/dist/components/overlays/Dialog.slots.mjs +129 -0
  179. package/dist/components/overlays/Dialog.slots.mjs.map +1 -0
  180. package/dist/components/overlays/Drawer.cjs +1 -1
  181. package/dist/components/overlays/Drawer.cjs.map +1 -1
  182. package/dist/components/overlays/Drawer.context.cjs +2 -0
  183. package/dist/components/overlays/Drawer.context.cjs.map +1 -0
  184. package/dist/components/overlays/Drawer.context.mjs +29 -0
  185. package/dist/components/overlays/Drawer.context.mjs.map +1 -0
  186. package/dist/components/overlays/Drawer.mjs +12 -187
  187. package/dist/components/overlays/Drawer.mjs.map +1 -1
  188. package/dist/components/overlays/Drawer.panel.cjs +2 -0
  189. package/dist/components/overlays/Drawer.panel.cjs.map +1 -0
  190. package/dist/components/overlays/Drawer.panel.mjs +159 -0
  191. package/dist/components/overlays/Drawer.panel.mjs.map +1 -0
  192. package/dist/components/overlays/Drawer.parts.cjs +2 -0
  193. package/dist/components/overlays/Drawer.parts.cjs.map +1 -0
  194. package/dist/components/overlays/Drawer.parts.mjs +152 -0
  195. package/dist/components/overlays/Drawer.parts.mjs.map +1 -0
  196. package/dist/components/overlays/Drawer.root.cjs +2 -0
  197. package/dist/components/overlays/Drawer.root.cjs.map +1 -0
  198. package/dist/components/overlays/Drawer.root.mjs +207 -0
  199. package/dist/components/overlays/Drawer.root.mjs.map +1 -0
  200. package/dist/components/overlays/Drawer.scroll.cjs +2 -0
  201. package/dist/components/overlays/Drawer.scroll.cjs.map +1 -0
  202. package/dist/components/overlays/Drawer.scroll.mjs +42 -0
  203. package/dist/components/overlays/Drawer.scroll.mjs.map +1 -0
  204. package/dist/components/table/useGridNavigation.cjs +1 -1
  205. package/dist/components/table/useGridNavigation.cjs.map +1 -1
  206. package/dist/components/table/useGridNavigation.mjs +67 -70
  207. package/dist/components/table/useGridNavigation.mjs.map +1 -1
  208. package/dist/hooks/useActiveDescendant.cjs +2 -0
  209. package/dist/hooks/useActiveDescendant.cjs.map +1 -0
  210. package/dist/hooks/useActiveDescendant.mjs +56 -0
  211. package/dist/hooks/useActiveDescendant.mjs.map +1 -0
  212. package/dist/hooks/useDismiss.cjs.map +1 -1
  213. package/dist/hooks/useDismiss.mjs.map +1 -1
  214. package/dist/hooks/useFocusTrap.cjs +1 -1
  215. package/dist/hooks/useFocusTrap.cjs.map +1 -1
  216. package/dist/hooks/useFocusTrap.mjs +115 -98
  217. package/dist/hooks/useFocusTrap.mjs.map +1 -1
  218. package/dist/hooks/useListbox.cjs +1 -1
  219. package/dist/hooks/useListbox.cjs.map +1 -1
  220. package/dist/hooks/useListbox.mjs +250 -210
  221. package/dist/hooks/useListbox.mjs.map +1 -1
  222. package/dist/hooks/useModalLayer.cjs +1 -1
  223. package/dist/hooks/useModalLayer.cjs.map +1 -1
  224. package/dist/hooks/useModalLayer.mjs +35 -22
  225. package/dist/hooks/useModalLayer.mjs.map +1 -1
  226. package/dist/hooks/useRestoreFocus.cjs +1 -1
  227. package/dist/hooks/useRestoreFocus.cjs.map +1 -1
  228. package/dist/hooks/useRestoreFocus.mjs +91 -78
  229. package/dist/hooks/useRestoreFocus.mjs.map +1 -1
  230. package/dist/hooks/useRovingGrid.cjs +2 -0
  231. package/dist/hooks/useRovingGrid.cjs.map +1 -0
  232. package/dist/hooks/useRovingGrid.mjs +69 -0
  233. package/dist/hooks/useRovingGrid.mjs.map +1 -0
  234. package/dist/hooks/useRovingTabIndex.cjs +1 -1
  235. package/dist/hooks/useRovingTabIndex.cjs.map +1 -1
  236. package/dist/hooks/useRovingTabIndex.mjs +102 -78
  237. package/dist/hooks/useRovingTabIndex.mjs.map +1 -1
  238. package/dist/index.cjs +1 -1
  239. package/dist/index.d.cts +4391 -394
  240. package/dist/index.d.ts +4391 -394
  241. package/dist/index.mjs +115 -101
  242. package/dist/lib/classNames.cjs +1 -1
  243. package/dist/lib/classNames.cjs.map +1 -1
  244. package/dist/lib/classNames.mjs +170 -74
  245. package/dist/lib/classNames.mjs.map +1 -1
  246. package/dist/lib/date.cjs +2 -0
  247. package/dist/lib/date.cjs.map +1 -0
  248. package/dist/lib/date.mjs +44 -0
  249. package/dist/lib/date.mjs.map +1 -0
  250. package/dist/lib/focus.cjs +1 -1
  251. package/dist/lib/focus.cjs.map +1 -1
  252. package/dist/lib/focus.mjs +5 -2
  253. package/dist/lib/focus.mjs.map +1 -1
  254. package/dist/lib/icons.cjs +1 -1
  255. package/dist/lib/icons.cjs.map +1 -1
  256. package/dist/lib/icons.mjs +13 -6
  257. package/dist/lib/icons.mjs.map +1 -1
  258. package/dist/lib/layers.cjs +1 -1
  259. package/dist/lib/layers.cjs.map +1 -1
  260. package/dist/lib/layers.mjs +53 -47
  261. package/dist/lib/layers.mjs.map +1 -1
  262. package/dist/styles.css +1 -1
  263. package/package.json +1 -1
  264. package/src/styles/tokens.css +25 -0
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @mortenbrudvik/waveui
2
2
 
3
- A React component library inspired by the Fluent UI 2 design language: 69 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
3
+ A React component library inspired by the Fluent UI 2 design language: 78 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
4
4
 
5
5
  > This is **not** Microsoft's official `@fluentui/react-components` package.
6
6
 
7
7
  - [Requirements](#requirements) · [Installation](#installation) · [Quick start](#quick-start)
8
8
  - [Styles](#styles) · [Theming](#theming) · [Global effects](#global-effects) · [Styling hooks](#styling-hooks)
9
9
  - [React Server Components](#react-server-components) · [Components](#components) · [Usage notes](#usage-notes)
10
- - [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
10
+ - [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.10](#upgrading-from-010) · [Upgrading from 0.9](#upgrading-from-09) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
11
11
 
12
12
  ## Requirements
13
13
 
@@ -209,7 +209,7 @@ Never load `legacy-tokens.css` in an app that defines shadcn/ui-style variables
209
209
  Generated from `src/styles/tokens.css`. A value followed by a ramp or token name is a reference (`var(--wave-brand-80)`); the hex is its default.
210
210
 
211
211
  <details>
212
- <summary>All 64 color tokens per theme</summary>
212
+ <summary>All 68 color tokens per theme</summary>
213
213
 
214
214
  | Token (`--wave-…`) | Light | Dark | High contrast |
215
215
  | ------------------------- | ------------------------------ | ------------------------------ | ------------------------------ |
@@ -219,6 +219,8 @@ Generated from `src/styles/tokens.css`. A value followed by a ramp or token name
219
219
  | `secondary` | `#f5f5f5` | `#333333` | `#000000` |
220
220
  | `muted` | `#f0f0f0` | `#383838` | `#1a1a1a` |
221
221
  | `muted-foreground` | `#616161` | `#adadad` | `#ffffff` |
222
+ | `muted-hover` | `#e0e0e0` | `#424242` | `#333333` |
223
+ | `muted-pressed` | `#d1d1d1` | `#2e2e2e` | `#4d4d4d` |
222
224
  | `primary` | `#0f6cbd` (brand-80) | `#62abf5` (brand-110) | `#1aebff` |
223
225
  | `primary-foreground` | `#ffffff` | `#000000` | `#000000` |
224
226
  | `primary-hover` | `#115ea3` (brand-70) | `#77b7f7` (brand-120) | `#6ef3ff` |
@@ -233,6 +235,8 @@ Generated from `src/styles/tokens.css`. A value followed by a ramp or token name
233
235
  | `subtle-selected` | `#ebebeb` | `#383838` | `#333333` |
234
236
  | `selected` | `#ebf3fc` (brand-160) | `#082338` (brand-20) | `#003a40` |
235
237
  | `selected-foreground` | `#0f548c` (brand-60) | `#62abf5` (brand-110) | `#ffffff` |
238
+ | `selected-hover` | `#cfe4fa` (brand-150) | `#0c3b5e` (brand-40) | `#005259` |
239
+ | `selected-pressed` | `#b4d6fa` (brand-140) | `#061724` (brand-10) | `#006b75` |
236
240
  | `border` | `#e0e0e0` | `#666666` | `#ffffff` |
237
241
  | `stroke` | `#d1d1d1` | `#666666` | `#ffffff` |
238
242
  | `stroke-hover` | `#c7c7c7` | `#757575` | `#ffffff` |
@@ -310,7 +314,7 @@ What adding Wave changes outside its own components:
310
314
  **Colliding utility names.** Wave's Tailwind color names are unprefixed, because they are the utility vocabulary 0.4 documented and that consumer code uses in `className` overrides. In a shared Tailwind build, or next to another stylesheet that defines the same class names, each of the following names resolves to a single definition in every utility that takes a color (`bg-`, `text-`, `border-`, `ring-`, `outline-`, `fill-`, `stroke-`, `accent-`, `caret-`, `decoration-`, `divide-`, `placeholder-`, `shadow-`, `from-`/`via-`/`to-`, …):
311
315
 
312
316
  - Shared with shadcn/ui: `background`, `foreground`, `card`, `card-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`.
313
- - Wave's other names: `primary-hover`, `primary-pressed`, `error`, `error-foreground`, `subtle`, `subtle-hover`, `subtle-pressed`, `subtle-selected`, `selected`, `selected-foreground`, `stroke`, `stroke-hover`, `stroke-accessible`, `success`, `success-foreground`, `success-tint`, `success-tint-foreground`, `warning`, `warning-foreground`, `warning-tint`, `warning-tint-foreground`, `error-tint`, `error-tint-foreground`, `severe`, `severe-foreground`, `severe-tint`, `severe-tint-foreground`, `info`, `info-foreground`, `info-tint`, `info-tint-foreground`, `inverted`, `inverted-foreground`, `inverted-border`, `input-filled-darker`, `input-filled-lighter`, `input-filled-stroke`, `track`, `skeleton`, `rating`, `presence-available`, `presence-busy`, `presence-away`, `presence-offline`, `presence-oof`, `presence-glyph`, `backdrop`, `brand-10` … `brand-160`.
317
+ - Wave's other names: `primary-hover`, `primary-pressed`, `error`, `error-foreground`, `subtle`, `subtle-hover`, `subtle-pressed`, `subtle-selected`, `selected`, `selected-foreground`, `selected-hover`, `selected-pressed`, `muted-hover`, `muted-pressed`, `stroke`, `stroke-hover`, `stroke-accessible`, `success`, `success-foreground`, `success-tint`, `success-tint-foreground`, `warning`, `warning-foreground`, `warning-tint`, `warning-tint-foreground`, `error-tint`, `error-tint-foreground`, `severe`, `severe-foreground`, `severe-tint`, `severe-tint-foreground`, `info`, `info-foreground`, `info-tint`, `info-tint-foreground`, `inverted`, `inverted-foreground`, `inverted-border`, `input-filled-darker`, `input-filled-lighter`, `input-filled-stroke`, `track`, `skeleton`, `rating`, `presence-available`, `presence-busy`, `presence-away`, `presence-offline`, `presence-oof`, `presence-glyph`, `backdrop`, `brand-10` … `brand-160`.
314
318
 
315
319
  Workarounds when your app has its own design system:
316
320
 
@@ -366,11 +370,12 @@ export function checkedRows(tree: HTMLElement) {
366
370
 
367
371
  ## React Server Components
368
372
 
369
- - Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster` and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle and render Wave components with serializable props.
373
+ - Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster`, the date helpers (`addDays`, `addMonths`, `isSameDay`, `startOfWeek`, `getWeekNumber`) and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle, compute dates and render Wave components with serializable props.
370
374
  - A Server Component **cannot dot into a client component**: `Card.Header` throws "Cannot access Header on the server". Every sub-component is therefore also exported under a flat name. Use the flat names in Server Components and either form in client components.
371
- - Every compound can be composed in a Server Component. Parts written there reach the client as lazy references, and the compounds recognise them, so the server HTML and the client behaviour are the same as in a client file: Accordion items, Carousel slides, the options of Combobox and Dropdown, DataGrid parts, Drawer and Menu triggers, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, Nav, TabList and Tree items, the stars of a Rating or RatingDisplay, the parts of Toast and MessageBar, the ColorPicker parts, `Tree.ItemPersonaLayout`, a `Label` or `InfoLabel` passed as the `label` of a Field, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
375
+ - Every compound can be composed in a Server Component. Parts written there reach the client as lazy references, and the compounds recognise them, so the server HTML and the client behaviour are the same as in a client file: Accordion items, Carousel slides, the options of Combobox, Dropdown and Listbox, the parts and options of TagPicker, DataGrid parts, Drawer and Menu triggers, the Drawer header, body and footer parts, the tags of a TagGroup and the halves of an InteractionTag, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, TabList and Tree items, Nav items, categories, section headers, dividers and app items, the header, body and footer of a NavDrawer, the stars of a Rating or RatingDisplay, the parts of Toast and MessageBar, the ColorPicker parts, `SwatchPicker.Row` and the swatches (`ColorSwatch`, `ImageSwatch`, `EmptySwatch`, in rows of the grid layout or the implicit ones), `Tree.ItemPersonaLayout`, a `Label` or `InfoLabel` passed as the `label` of a Field, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
372
376
  - Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component.
373
- - A `Popover`, `Dialog`, `Drawer`, popup `Menu`, `Combobox`, `Dropdown`, `DatePicker`, `TimePicker` or `TagPicker` rendered on the server with `defaultOpen` (or `open`) is closed in the server HTML and opens once hydrated, so no ARIA reference points at a popup that is not there. `onOpenChange` is not called for it.
377
+ - A `Popover`, `Dialog`, overlay `Drawer` or `NavDrawer`, popup `Menu`, `Combobox`, `Dropdown`, `DatePicker`, `TimePicker` or `TagPicker` rendered on the server with `defaultOpen` (or `open`) is closed in the server HTML and opens once hydrated, so no ARIA reference points at a popup that is not there. `onOpenChange` is not called for it.
378
+ - An inline `Drawer` or `NavDrawer` (`type="inline"`) renders in the server HTML: open, or `hidden` when it is closed and kept mounted (`unmountOnClose={false}`).
374
379
 
375
380
  ```tsx
376
381
  // app/layout.tsx (Server Component)
@@ -448,12 +453,15 @@ export function SaveArea() {
448
453
  | `Combobox` | `ComboboxOption`, `ComboboxOptionGroup` (also `Option`, `OptionGroup`) |
449
454
  | `DataGrid` | `DataGridHeader`, `DataGridHeaderCell`, `DataGridBody`, `DataGridRow`, `DataGridCell` |
450
455
  | `Dialog` | `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogFooter`, `DialogClose` |
451
- | `Drawer` | `DrawerTrigger`, `DrawerTitle`, `DrawerClose` |
456
+ | `Drawer` | `DrawerTrigger`, `DrawerTitle`, `DrawerClose`, `DrawerHeader`, `DrawerHeaderTitle`, `DrawerHeaderNavigation`, `DrawerBody`, `DrawerFooter` |
452
457
  | `Dropdown` | `DropdownOption`, `DropdownOptionGroup` |
458
+ | `InteractionTag` | `InteractionTagPrimary`, `InteractionTagSecondary` |
453
459
  | `List` | `ListItem` |
460
+ | `Listbox` | `ListboxOption`, `ListboxOptionGroup` (also `Option`, `OptionGroup`) |
454
461
  | `Menu` | `MenuTrigger`, `MenuPopover`, `MenuItem`, `MenuDivider`, `MenuItemCheckbox`, `MenuItemRadio`, `MenuItemSwitch`, `MenuItemLink`, `MenuGroup`, `MenuGroupHeader`, `MenuSplitGroup` |
455
462
  | `MessageBar` | `MessageBarTitle`, `MessageBarActions` |
456
- | `Nav` | `NavCategory`, `NavItem`, `NavSubItem` |
463
+ | `Nav` | `NavCategory`, `NavItem`, `NavSubItem`, `NavSectionHeader`, `NavDivider`, `NavAppItem`, `NavAppItemStatic` |
464
+ | `NavDrawer` | `NavDrawerHeader`, `NavDrawerBody`, `NavDrawerFooter` |
457
465
  | `Overflow` | `OverflowItem` |
458
466
  | `Popover` | `PopoverTrigger`, `PopoverContent` |
459
467
  | `RadioGroup` | `RadioGroupItem` (also `RadioItem`) |
@@ -461,15 +469,17 @@ export function SaveArea() {
461
469
  | `RatingDisplay` | `RatingDisplayItem` (also `RatingItem`) |
462
470
  | `Skeleton` | `SkeletonGroup` |
463
471
  | `Stepper` | `StepperStep` |
472
+ | `SwatchPicker` | `SwatchPickerRow` |
464
473
  | `Table` | `TableHeader`, `TableHeaderCell`, `TableBody`, `TableRow`, `TableCell` |
465
474
  | `TabList` | `TabListTab`, `TabListPanel`, `TabListPanels` |
475
+ | `TagPicker` | `TagPickerControl`, `TagPickerGroup`, `TagPickerInput`, `TagPickerButton`, `TagPickerList`, `TagPickerOption`, `TagPickerOptionGroup` (also `OptionGroup`) |
466
476
  | `Toast` | `ToastTitle`, `ToastBody`, `ToastFooter`, `ToastClose` |
467
477
  | `Toolbar` | `ToolbarButton`, `ToolbarToggleButton`, `ToolbarRadioGroup`, `ToolbarRadioButton`, `ToolbarGroup`, `ToolbarDivider` |
468
478
  | `Tree` | `TreeItem`, `TreeItemPersonaLayout` |
469
479
 
470
480
  ## Components
471
481
 
472
- 69 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
482
+ 78 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
473
483
 
474
484
  Your `className` is merged last with `cn()`, so it replaces a conflicting class **of the same variant**: `bg-error` replaces a resting `bg-primary`. A class behind a variant is replaced only by a class with the same variant. That matters for the gated hover and pressed classes (`not-disabled:not-aria-disabled:hover:…` / `…:active:…`) and the state classes (`data-[selected]:…`, `aria-disabled:…`): they stay next to yours and win while their state applies, because they are more specific. A bare `hover:bg-error` therefore does not override a Button's built-in hover color while the Button is enabled. It still paints on hover while the Button is disabled or `aria-disabled`, and on `appearance="transparent"`, which has no hover background. To override the gated and state classes, use the same prefix (`not-disabled:not-aria-disabled:hover:bg-error`, `data-[selected]:bg-error`) or the important modifier (`hover:bg-error!`); see [Buttons](#buttons).
475
485
 
@@ -500,16 +510,21 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
500
510
  | `SearchBox` | Search input with a clear button; `size`, `appearance`, `onValueChange`; input attributes go to the `<input>`. The root draws the field, so `className` and `style` style the field box, and `contentBefore`/`contentAfter` sit beside the text. |
501
511
  | `Slider` | Styled native range input, filled up to the thumb; `size` (`small`, `medium`); `onValueChange(number)`. |
502
512
  | `SpinButton` | Numeric input with step buttons; `size`, `appearance`; typed text is a draft committed on blur or Enter; `min`, `max`, `step`, `largeStep`, `precision`; `displayValue` shows formatted text; `allowEmpty` allows a `null` value; holding a step button repeats it (see [SpinButton](#spinbutton)). |
503
- | `Combobox` | Editable combobox: typing filters the options; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup`. |
504
- | `Dropdown` | Select-only combobox (a button) with typeahead; `clearable`. `Dropdown.Option`, `Dropdown.OptionGroup`. |
505
- | `TagPicker` | Multi-select combobox that shows the selection as removable tags (a list named "Selected"); `size` (`medium`, `large`, `extra-large`), `appearance`; the input is described by a summary of the selection ("Selected: Apple, Banana"), and additions and removals are announced. |
506
- | `DatePicker` | Date input with a calendar dialog; `size`, `appearance`; locale-aware format and parse, `minDate`, `maxDate`, `disabledDates`, `onInvalidInput`. |
513
+ | `Combobox` | Editable combobox: typing filters the options (`filter` replaces the match, `query` controls the typed text); `multiselect` selects several values; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup` (see [Multi-select pickers](#multi-select-pickers) and [Filtering and async search](#filtering-and-async-search)). |
514
+ | `Dropdown` | Select-only combobox (a button) with typeahead; `multiselect` selects several values; `clearable`, `expandIcon` and `renderValue`. `Dropdown.Option`, `Dropdown.OptionGroup`. |
515
+ | `Listbox` | A list of options in the page that holds focus itself (`aria-activedescendant`), single-select or `multiselect`, with typeahead, groups and native form support; `Listbox.Option`, `Listbox.OptionGroup`. The listbox primitives under the pickers are public, for pickers of your own (see [Listbox and custom pickers](#listbox-and-custom-pickers)). |
516
+ | `TagPicker` | Multi-select combobox that shows the selection as removable tags (Tags in a TagGroup list named "Selected", one Tag size below the picker); `size` (`medium`, `large`, `extra-large`), `appearance`; the input is described by a summary of the selection ("Selected: Apple, Banana"), and additions and removals are announced; `filter` and `query` as in Combobox. A chevron opens the list (`expandIcon`), `clearable` removes every tag, `overflow="count"` keeps the tags on one line with a "+N" count, and `allowCreate` with `onOptionCreate` adds a tag of the user's own text. Composable: `TagPicker.Control`, `.Group`, `.Input` or `.Button` (a trigger without text entry), `.List`, `.Option` (`media`, `secondaryContent`) and `.OptionGroup`; a part without children renders its defaults (see [TagPicker parts and free tagging](#tagpicker-parts-and-free-tagging)). |
517
+ | `DatePicker` | Date input with a calendar dialog (month and year views, week numbers, marked days, optional "Go to today" and Close buttons); `size`, `appearance`; locale-aware format and parse, `minDate`, `maxDate`, `disabledDates`, `onInvalidInput`. |
518
+ | `Calendar` | The DatePicker's calendar in the page (`role="group"`): month and year views, week numbers, marked days and custom day content, week, work-week or month ranges, "Go to today", native form support (see [Calendar](#calendar)). |
507
519
  | `TimePicker` | Time combobox (`12h`/`24h`, `step`, `minTime`, `maxTime`); `size`, `appearance`; value is `HH:mm`; `open`/`defaultOpen`/`onOpenChange` control the list; rejected text is kept and flagged (`onInvalidInput`). |
508
520
  | `ColorPicker` | Hex value (lowercase `#rrggbb`, or `#rrggbbaa` when not opaque) with a color area, a hue slider, a hex field, preset swatches and an optional opacity slider; with children, your own layout of `ColorArea`, `ColorSlider` and `AlphaSlider` (bound to the picker) and `ColorPicker.HexField`, `.Swatches`, `.Preview` (see [Color](#color)); `shape`. Picking a preset keeps the current opacity (a preset's alpha digits are ignored, with a development warning). |
509
521
  | `ColorArea` | 2-D saturation and brightness field for an `HsvColor`, with two hidden range inputs (one Tab stop), pointer and touch dragging; mirrored in RTL. |
510
522
  | `ColorSlider` | Hue (default), saturation or brightness slider (`channel`), horizontal or vertical. |
511
523
  | `AlphaSlider` | Opacity slider over a checkerboard; `transparency` shows transparency. |
512
- | `SwatchPicker` | Radio group of color swatches (`items` with a `label` each). |
524
+ | `SwatchPicker` | Color swatches from `items` or children: a radio group (the default row layout) or a grid of `SwatchPicker.Row` rows (`layout="grid"`); `focusMode="tab"` makes every swatch a Tab stop; four sizes and `spacing` (see [Swatches](#swatches)). |
525
+ | `ColorSwatch` | A color swatch of a SwatchPicker (`value`, `color`), with an optional `icon` and `borderColor`; `disabled` crosses it out. A `Tooltip` around it can name it. |
526
+ | `ImageSwatch` | An image swatch of a SwatchPicker: a pattern or texture (`src`) drawn as a covering background. |
527
+ | `EmptySwatch` | An "add" button in a SwatchPicker (named "Add color" by default) that selects nothing: your `onClick` opens a color dialog, for example. |
513
528
  | `Rating` | Star rating (`role="radiogroup"`); `RatingDisplay` is the read-only version (a fractional value draws a partly filled star; `showValue`, `count` and `compact` add the value and the number of ratings as text). |
514
529
 
515
530
  ### Data display
@@ -521,7 +536,9 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
521
536
  | `Badge` | Status or category label: `appearance` `filled`, `tint`, `outline`; semantic `color` (see [Badges](#badges)). |
522
537
  | `CounterBadge` | Count pill (`99+` above `overflowCount`) or, with `dot`, an unread dot; Badge's `color` palette; `showZero`. |
523
538
  | `PresenceBadge` | Availability badge with a distinct shape per status. |
524
- | `Tag` | Chip with an optional dismiss button (`dismissible`, `onDismiss`, `dismissLabel`); `onDismiss` and `dismissIcon` need `dismissible` (a development warning says so). Move focus when you remove a tag (see [Tags](#tags)). |
539
+ | `Tag` | Chip: `appearance` (`filled`, `outline`, `brand`), `size` (`medium` 28px, `small`, `extra-small`), `shape` (`circular`, `rounded`), `disabled`, a look-only `selected`, `value` (its key in a TagGroup) and the `media` (an Avatar), `icon` and `secondaryText` slots; an optional Dismiss button (`dismissible`, `onDismiss`, `dismissLabel`) that also dismisses on Delete and Backspace. `onDismiss` and `dismissIcon` need `dismissible` (a development warning says so). In a TagGroup focus moves for you when a tag is removed; on its own, move it yourself (see [Tags](#tags)). |
540
+ | `TagGroup` | A set of tags: a toolbar (one Tab stop, arrow keys) or, with `role="list"`, a list; default `size`, `appearance`, `shape`, `dismissible` and `disabled` for its tags; `onDismiss(value)`, focus to the next tag (else the previous one, else `fallbackFocusRef`) when the focused tag is removed; selectable InteractionTags through `value`/`defaultValue`/`onValueChange`, with `name`, `form` and `required`. |
541
+ | `InteractionTag` | A tag with an action: `InteractionTag.Primary` (a button, or a toggle in a selecting TagGroup; `media`, `icon`, `secondaryText`) and an optional `InteractionTag.Secondary` (a separate Dismiss button named "Dismiss <text>"); `size`, `appearance`, `shape`, `disabled`, `selected`, `value`. |
525
542
  | `InfoLabel` | A real `<label>` (`children`, `htmlFor`, `required`, `size`, `weight`, `disabled`) with an info button after it that shows `info` (text, links or other content) in a note; `openOnHover`, `infoButtonLabel`. Usable as the `label` of a Field. |
526
543
  | `InfoButton` | A button with the info glyph that shows `info` in a `role="note"` next to it, without moving focus: opens on click, on mouse hover and on keyboard focus (`openOnHover={false}`: click only); Tab from the button enters the note; `size`. Named "Information" (`aria-label`). |
527
544
  | `Persona` | Avatar with name, secondary text and presence. |
@@ -565,7 +582,9 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
565
582
  | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
566
583
  | `Breadcrumb` | Trail of links, buttons or text (`Breadcrumb.Item`, `current`). |
567
584
  | `Menu` | Popup menu (`Menu.Trigger`, `Menu.Popover`, which scrolls when it is taller than the viewport) or static menu; `Menu.Item`, `Menu.Divider`; checkbox, radio and switch items bound to `checkedValues`, `Menu.Group`, `Menu.ItemLink`, submenus, `Menu.SplitGroup`, hover opening and context menus (see [Menus](#menus)). |
568
- | `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked. |
585
+ | `Nav` | Side navigation with items, collapsible categories and sub-items; links or buttons; a collapsed category that holds the current page is marked. `Nav.SectionHeader` (a heading, `headingLevel`), `Nav.Divider` (decorative), `Nav.AppItem` and `Nav.AppItemStatic` (the app's name at the top), and `actions` on `Nav.Item` and `Nav.SubItem` (your buttons beside the link, shown on hover and focus). Every child of its list is a list item (a development warning says so). |
586
+ | `NavDrawer` | Nav in a drawer panel (see [Navigation drawers and app shells](#navigation-drawers-and-app-shells)): `type` `overlay` (a modal or non-modal dialog around a `<nav>`) or `inline` (part of the page layout), `position` (default `start`), `size` (default `small`, 320px), `separator`, `modalType`, `unmountOnClose`, `finalFocusRef`, `backdrop`, Nav's value and category props and `onItemSelect(value)` on every activation; `NavDrawer.Header`, `NavDrawer.Body` (`focusMode` `tab` or `arrow`) and `NavDrawer.Footer`. |
587
+ | `Hamburger` | A `subtle` icon-only Button with the navigation glyph, named "Navigation"; inside a NavDrawer it toggles the drawer (`aria-expanded`, `aria-controls`). |
569
588
  | `Stepper` | Multi-step progress (`Stepper.Step`), horizontal or vertical, optionally `linear`. |
570
589
  | `Pagination` | Page buttons with ellipses, previous/next and optional first/last. |
571
590
 
@@ -573,8 +592,8 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
573
592
 
574
593
  | Component | Description |
575
594
  | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
576
- | `Dialog` | Modal dialog (`modalType="alert"` for a confirmation): `Dialog.Trigger`, `Dialog.Content` (`closeLabel` names its Close button), `Dialog.Title`, `Dialog.Footer` (stays in view), `Dialog.Close`. |
577
- | `Drawer` | Modal side panel (`position` `start`, `end`, `left`, `right`; `closeLabel`); `Drawer.Trigger`, `Drawer.Title`, `Drawer.Close`. |
595
+ | `Dialog` | Modal dialog (`modalType="alert"` for a confirmation, `"non-modal"` for one the page stays usable around): `Dialog.Trigger` (toggles), `Dialog.Content` (`size` `small`, `medium`, `large`, `full`; `closeLabel` names its Close button, `closeButton` replaces or hides it, `backdrop`, `titleAction`, `unmountOnClose`), `Dialog.Title` (`action`), `Dialog.Footer` (stays in view), `Dialog.Close` (see [Dialogs and triggers](#dialogs-and-triggers)). |
596
+ | `Drawer` | Side or bottom panel (see [Drawers](#drawers)): `type` `overlay` (`modalType` `modal`, `alert` or `non-modal`) or `inline`; `position` (`start`, `end`, `left`, `right`, `bottom`), `size` (`small` 320px, `medium`, `large`, `full`), `separator`, `backdrop`, `closeButton`, `closeLabel`, `unmountOnClose`; `Drawer.Trigger` (toggles), `Drawer.Title`, `Drawer.Close`, and the layout parts `Drawer.Header`, `Drawer.HeaderTitle`, `Drawer.HeaderNavigation`, `Drawer.Body` and `Drawer.Footer`, whose separators show while content is scrolled under them. |
578
597
  | `Popover` | Non-modal popup anchored to `Popover.Trigger` or a `target`; `side`, `align`; hover cards (`openOnHover`) and context popovers (`openOnContext`). |
579
598
  | `Tooltip` | Hover and focus text for its child, as a description or (`relationship="label"`) a name; `open`/`defaultOpen`/`onOpenChange`; `openDelay`, `closeDelay`. |
580
599
  | `TeachingPopover` | Step-by-step onboarding popover, optionally pointing at a `target`. |
@@ -698,7 +717,7 @@ export function AccountSettings({ backedUp }: { backedUp: number }) {
698
717
  }
699
718
  ```
700
719
 
701
- - Library controls inside a `Field` are named by its label, described by its validation message and hint, and marked `aria-invalid`/`aria-required`. With `required`, native inputs (`Input`, `Select`, `Textarea`, `Slider`, `SpinButton`, `SearchBox`) also get the native `required` attribute, so the browser blocks an empty submit. The choice and picker controls (`Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SwatchPicker`, `ColorPicker`, `ColorArea`, `ColorSlider`, `AlphaSlider`, `Combobox`, `Dropdown`, `TagPicker`, `DatePicker`, `TimePicker`) are required too, also without `name`: the browser blocks the submit until the control is checked, switched on or has a value, so `<Field required><Switch /></Field>` means "must be on". Like a native read-only input, a `readOnly` Combobox, TagPicker, DatePicker or TimePicker does not block the submit (it keeps `aria-required`). Add `noValidate` to a `<form>` that validates in its submit handler.
720
+ - Library controls inside a `Field` are named by its label, described by its validation message and hint, and marked `aria-invalid`/`aria-required`. With `required`, native inputs (`Input`, `Select`, `Textarea`, `Slider`, `SpinButton`, `SearchBox`) also get the native `required` attribute, so the browser blocks an empty submit. The choice and picker controls (`Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SwatchPicker`, `ColorPicker`, `ColorArea`, `ColorSlider`, `AlphaSlider`, `Combobox`, `Dropdown`, `Listbox`, `TagPicker`, `DatePicker`, `Calendar`, `TimePicker`) are required too, also without `name`: the browser blocks the submit until the control is checked, switched on or has a value, so `<Field required><Switch /></Field>` means "must be on". Like a native read-only input, a `readOnly` Combobox, TagPicker, DatePicker or TimePicker does not block the submit (it keeps `aria-required`). Add `noValidate` to a `<form>` that validates in its submit handler.
702
721
  - **Validation states.** `validationMessage` renders below the control in the style of `validationState`: `error` (the default when a message is given) marks the control `aria-invalid` and announces the message (`role="alert"`); `warning` announces it without making the control invalid; `success` and `none` show it without announcing it. Each state but `none` shows an icon before the message; `validationMessageIcon` replaces it, and `null` removes it. `validationState="error"` without a message marks the control invalid and shows nothing. The Field root carries `data-validation-state`.
703
722
  - **`error`** is the shorthand for an error message: `error="Enter an email address"`, or `error={true}` to mark the control invalid without a message. It wins over `validationMessage` and `validationState` (a development warning fires when both are set).
704
723
  - **The hint** stays visible below the message, after it; the control is described by the message, then the hint.
@@ -728,7 +747,7 @@ export function AccountSettings({ backedUp }: { backedUp: number }) {
728
747
 
729
748
  - One control per Field. Put wrapper or layout components (a `Tooltip`, your own row) **inside a plain `<div>`** in the Field; the library control inside is then labelled correctly.
730
749
  - `error` on `Input`, `Select` and `Textarea`: a string renders the message after the control in a `role="alert"` element linked with `aria-describedby`/`aria-errormessage`; `error={true}` only marks the control invalid. Inside a Field that shows its own error, the message is not repeated.
731
- - Value controls take part in native forms when you pass `name`, or when they are required (their own `required`, or a required `Field`): `Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SpinButton`, `SwatchPicker`, `ColorPicker`, `ColorArea`, `ColorSlider` and `AlphaSlider` (the hex of the color; inside a ColorPicker they are bound and the picker submits), `Combobox`, `Dropdown`, `TagPicker`, `DatePicker` (ISO `yyyy-mm-dd`) and `TimePicker` (`HH:mm`) render a hidden input, honour `required` and `form`, and reset with their form. `SearchBox` submits through its own `<input name>` and resets with its form too. No name is generated for you.
750
+ - Value controls take part in native forms when you pass `name`, or when they are required (their own `required`, or a required `Field`): `Checkbox`, `Switch`, `RadioGroup`, `Rating`, `SpinButton`, `SwatchPicker`, `ColorPicker`, `ColorArea`, `ColorSlider` and `AlphaSlider` (the hex of the color; inside a ColorPicker they are bound and the picker submits), `Combobox`, `Dropdown` and `Listbox` (one input per value with `multiselect`), `TagPicker`, `DatePicker` and `Calendar` (ISO `yyyy-mm-dd`) and `TimePicker` (`HH:mm`) render a hidden input, honour `required` and `form`, and reset with their form. `SearchBox` submits through its own `<input name>` and resets with its form too. No name is generated for you.
732
751
 
733
752
  ### Sizes and appearances
734
753
 
@@ -739,7 +758,7 @@ The text inputs and pickers (`Input`, `Textarea`, `Select`, `SearchBox`, `SpinBu
739
758
  | `size` | `'small'` (24px tall), `'medium'` (32px), `'large'` (40px); type `CoreSize` | `medium` |
740
759
  | `appearance` | `'outline'`, `'underline'`, `'filled-darker'`, `'filled-lighter'`; type `InputAppearance` | `outline` |
741
760
 
742
- `medium` and `outline` are the 0.7 look. `Textarea` sets minimum heights (64, 80 and 96px) instead of a height. `TagPicker` has no `small` (`TagPickerSize`: `'medium'`, `'large'`, `'extra-large'`). The choice controls have their own unions: `Checkbox` `size` `'medium'` (an 18px box) or `'large'` (22px) and `shape` `'square'` or `'circular'`; `Switch` `size` `'small'` (a 32×16px track) or `'medium'` (40×20px); `Slider` `size` `'small'` (a 16px thumb on a 2px rail) or `'medium'` (a 20px thumb on a 4px rail). The icon buttons of the pickers (clear, expand, calendar) scale with `size` and keep a 24×24px target at every size; the step buttons of `SpinButton` are 24, 32 and 40px wide.
761
+ `medium` and `outline` are the 0.7 look. `Textarea` sets minimum heights (64, 80 and 96px) instead of a height. `TagPicker` has no `small` (`TagPickerSize`: `'medium'`, `'large'`, `'extra-large'`), and its tags are one Tag size below it: `extra-small` (20px, 12px text) in a medium picker, `small` (24px) in a large one, `medium` (28px) in an extra-large one; they are filled, or outlined on `filled-darker`. The choice controls have their own unions: `Checkbox` `size` `'medium'` (an 18px box) or `'large'` (22px) and `shape` `'square'` or `'circular'`; `Switch` `size` `'small'` (a 32×16px track) or `'medium'` (40×20px); `Slider` `size` `'small'` (a 16px thumb on a 2px rail) or `'medium'` (a 20px thumb on a 4px rail). The icon buttons of the pickers (clear, expand, calendar) scale with `size` and keep a 24×24px target at every size; the step buttons of `SpinButton` are 24, 32 and 40px wide.
743
762
 
744
763
  ```tsx
745
764
  import { Field, Input, WaveProvider } from '@mortenbrudvik/waveui';
@@ -835,24 +854,316 @@ export function Reviews() {
835
854
 
836
855
  `onChange` is the native DOM change event (`Input`, `Textarea`, `Select`, `Slider`). State callbacks are named after the state and receive the value:
837
856
 
838
- | State | Props |
839
- | ------------------------------ | ------------------------------------------------------------------------------------------ |
840
- | single value | `value` / `defaultValue` / `onValueChange(value)` |
841
- | checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` |
842
- | open | `open` / `defaultOpen` / `onOpenChange(open)` |
843
- | checked values (Menu, Toolbar) | `checkedValues` / `defaultCheckedValues` / `onCheckedValuesChange(checkedValues, details)` |
844
- | checked items (Tree) | `checkedItems` / `defaultCheckedItems` / `onCheckedItemsChange(checkedItems, details)` |
857
+ | State | Props |
858
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
859
+ | single value | `value` / `defaultValue` / `onValueChange(value)` |
860
+ | several values (`multiselect` Dropdown, Combobox, Listbox) | `value` / `defaultValue` / `onValueChange(values)` |
861
+ | typed text (Combobox, TagPicker) | `query` / `defaultQuery` / `onQueryChange(query)` |
862
+ | checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` |
863
+ | open | `open` / `defaultOpen` / `onOpenChange(open)` |
864
+ | checked values (Menu, Toolbar) | `checkedValues` / `defaultCheckedValues` / `onCheckedValuesChange(checkedValues, details)` |
865
+ | checked items (Tree) | `checkedItems` / `defaultCheckedItems` / `onCheckedItemsChange(checkedItems, details)` |
845
866
 
846
- Extra data goes in a second, optional `details` argument: Dialog and Drawer call `onOpenChange(open, details)` with `details.reason` and `details.event` (see [Dialogs and triggers](#dialogs-and-triggers)), Menu and Toolbar call `onCheckedValuesChange(checkedValues, details)` with `details.name`, `details.checkedItems` (the group's new values) and `details.event` (see [Menus](#menus)), and Tree calls `onCheckedItemsChange(checkedItems, details)` with `details.value`, `details.checked` and `details.event` (see [Trees](#trees)). These fire only when the value changes. Event callbacks such as `onPageChange`, `onStepChange` and `Tree`'s `onItemSelect` fire on every activation, also when the current item is activated again. Array props that Wave only reads (`openItems`, `expandedItems`, `checkedItems`, `selectedItems`, `columns`, `options`, `items`, `steps`, …) accept readonly arrays such as `as const` values; the callbacks receive a new, mutable array. The 0.4 names still work as deprecated aliases that warn once in development; see the [CHANGELOG](CHANGELOG.md) for the full table.
867
+ Extra data goes in a second, optional `details` argument: Dialog and Drawer call `onOpenChange(open, details)` with `details.reason` and `details.event` (see [Dialogs and triggers](#dialogs-and-triggers)), Menu and Toolbar call `onCheckedValuesChange(checkedValues, details)` with `details.name`, `details.checkedItems` (the group's new values) and `details.event` (see [Menus](#menus)), Tree calls `onCheckedItemsChange(checkedItems, details)` with `details.value`, `details.checked` and `details.event` (see [Trees](#trees)), and Calendar calls `onValueChange(date, details)` with `details.range`, the selected days (see [Calendar](#calendar)). These fire only when the value changes. Event callbacks such as `onPageChange`, `onStepChange`, `Tree`'s and `NavDrawer`'s `onItemSelect` and `TagGroup`'s `onDismiss` fire on every activation, also when the current item is activated again. Array props that Wave only reads (`openItems`, `expandedItems`, `checkedItems`, `selectedItems`, `columns`, `options`, `items`, `steps`, …) accept readonly arrays such as `as const` values; the callbacks receive a new, mutable array. The 0.4 names still work as deprecated aliases that warn once in development; see the [CHANGELOG](CHANGELOG.md) for the full table.
847
868
 
848
869
  ### Composite controls
849
870
 
850
- Checkbox, Switch, SearchBox, SpinButton, Combobox, Dropdown, TagPicker, DatePicker and TimePicker render a wrapper around their focusable element. `id`, the naming and validation ARIA attributes (`aria-label`, `aria-labelledby`, `aria-describedby`, `aria-invalid`, `aria-required`, `aria-errormessage`, `aria-details`; on Checkbox and Switch also `aria-disabled`), `tabIndex`, `autoFocus`, focus and key handlers (and native input attributes for text fields) go to the focusable element; `className`, `style`, `hidden`, `data-*`, other `aria-*` attributes and `ref` stay on the wrapper. `controlRef` gives you the focusable element. SearchBox and SpinButton draw their field (border, background, focus and invalid look) on that wrapper, so `className`, `style` and `hidden` size, style and hide the visible field, as on Input's slot wrapper.
871
+ Checkbox, Switch, SearchBox, SpinButton, Combobox, Dropdown, TagPicker, DatePicker and TimePicker render a wrapper around their focusable element. `id`, the naming and validation ARIA attributes (`aria-label`, `aria-labelledby`, `aria-describedby`, `aria-invalid`, `aria-required`, `aria-errormessage`, `aria-details`; on Checkbox and Switch also `aria-disabled`), `tabIndex`, `autoFocus`, focus and key handlers (and native input attributes for text fields) go to the focusable element; `className`, `style`, `hidden`, `data-*`, other `aria-*` attributes and `ref` stay on the wrapper. `controlRef` gives you the focusable element. A composite with trigger parts (TagPicker) sends them to whichever trigger renders: `TagPicker.Input`, or `TagPicker.Button` (pass handlers and `ref` on the Button itself). SearchBox and SpinButton draw their field (border, background, focus and invalid look) on that wrapper, so `className`, `style` and `hidden` size, style and hide the visible field, as on Input's slot wrapper.
851
872
 
852
873
  `Input` with `contentBefore`/`contentAfter` content also renders a bordered wrapper `<span>`, which receives `className`, `style` and `hidden` (so they size and hide the visible field); unlike the controls above, `ref`, `id`, `aria-*` and `data-*` stay on the `<input>`.
853
874
 
854
875
  `ColorSlider`, `AlphaSlider` and `ColorPicker.HexField` route the same props to their `<input>` and take `controlRef`. `ColorArea` is a group around two inputs: the group keeps `aria-label`, `aria-labelledby` and `aria-describedby` (a Field's label names it), with `className`, `style`, `data-*` and `ref`; both inputs get `aria-invalid` and `aria-errormessage`; `autoFocus` and `controlRef` reach the saturation input; `tabIndex` reaches the input that holds the Tab stop.
855
876
 
877
+ `Listbox` has no wrapper: every prop, `ref` and handler go to its `<ul role="listbox">`, which has focus. `Calendar` keeps them on its `role="group"` root, which the Field's label names. A swatch (`ColorSwatch`, `ImageSwatch`, `EmptySwatch`) puts its `ref`, `className`, `style`, name and other props on its own `<button>` in every layout (in the grid layout, a `role="gridcell"` element wraps it), so a `Tooltip` around a swatch reaches the element that has focus.
878
+
879
+ ### Multi-select pickers
880
+
881
+ ```tsx
882
+ import * as React from 'react';
883
+ import { Combobox, Dropdown, Field } from '@mortenbrudvik/waveui';
884
+
885
+ export function PizzaForm() {
886
+ const [toppings, setToppings] = React.useState<string[]>(['cheese']);
887
+ return (
888
+ <form>
889
+ <Field label="Toppings">
890
+ <Dropdown
891
+ multiselect
892
+ value={toppings}
893
+ onValueChange={setToppings}
894
+ name="toppings"
895
+ clearable
896
+ >
897
+ <Dropdown.Option value="cheese">Cheese</Dropdown.Option>
898
+ <Dropdown.Option value="olives">Olives</Dropdown.Option>
899
+ <Dropdown.Option value="peppers">Peppers</Dropdown.Option>
900
+ </Dropdown>
901
+ </Field>
902
+ <Field label="Deliver to" required>
903
+ <Combobox multiselect defaultValue={['oslo']} name="cities">
904
+ <Combobox.Option value="oslo">Oslo</Combobox.Option>
905
+ <Combobox.Option value="bergen">Bergen</Combobox.Option>
906
+ <Combobox.Option value="trondheim">Trondheim</Combobox.Option>
907
+ </Combobox>
908
+ </Field>
909
+ </form>
910
+ );
911
+ }
912
+ ```
913
+
914
+ - **Values.** `multiselect` makes `value`, `defaultValue` and `onValueChange` arrays, in selection order: checking an option appends its value, unchecking removes it. Write `multiselect` as a literal: `multiselect={flag}` with a `boolean` variable is a type error, since the two modes take different value types; render two elements instead. `React.ComponentProps<typeof Dropdown>` is still the single-select `DropdownProps`, and `DropdownProps<true>` (`ComboboxProps<true>`, `ListboxProps<true>`) is the multi-select shape.
915
+ - **Toggling.** The listbox is `aria-multiselectable`, and every option draws a checkbox square with the check inside (`checkIcon` replaces the check). Enter, Space (Dropdown) and a click toggle an option and keep the list open; Tab, Escape and Alt+ArrowUp close it without committing.
916
+ - **What shows.** The Dropdown button and the Combobox input show the selected labels in selection order, joined with `", "` (`labels.selection`; values without an option are left out), in place of the placeholder. Dropdown's `renderValue(values)` replaces that content.
917
+ - **Typing in a multi-select Combobox.** Focusing the input selects the labels, so typing, pasting or an IME composition replaces them with a query that filters the options; a toggle, a close or a blur clears the query and shows the labels again, selected while the input has focus. Backspace and Delete only edit the text: the options, the clear button and a form reset change the value. `freeform` is not available with `multiselect`.
918
+ - **Announcements.** Each toggle the user makes is announced through a polite live region ("Olives added, 2 selected", from `labels.added` and `labels.removed`); a controlled change, a clear or a form reset is not. A single-select picker adds no announcer regions to `document.body`.
919
+ - **Forms.** `clearable` clears every value. A form submits one entry per value (read them with `FormData.getAll(name)`), `required` needs one value, and a form reset restores `defaultValue`, compared by content, so restoring the same values reports nothing. The deprecated `onOptionSelect` fires on every toggle.
920
+ - **For both modes.** `onActiveOptionChange(value)` (Dropdown, Combobox, Listbox) reports the option `aria-activedescendant` points at, after it changes (opening, the keys, typeahead, the pointer, a filter change), and `null` when the list closes. `disabledOptionsFocusable` keeps disabled options in the keys (the arrow keys, Home/End, PageUp/PageDown and typeahead; in a Combobox the arrow keys), but they are never selected: Enter, Space and a click do nothing on them and the list stays open. Dropdown's `expandIcon` replaces the chevron as Combobox's does (`false` hides it), and `renderValue(value)` renders the button's content while a value is selected; keep it non-interactive, since its text is the combobox's value.
921
+
922
+ ### Filtering and async search
923
+
924
+ Combobox and TagPicker take a `filter` of yours and a controllable query. A search on a server keeps every option the server returns (`filter={() => true}`) and searches as the query changes:
925
+
926
+ ```tsx
927
+ import * as React from 'react';
928
+ import { Combobox } from '@mortenbrudvik/waveui';
929
+
930
+ export function CountrySearch({ search }: { search: (query: string) => Promise<string[]> }) {
931
+ const [value, setValue] = React.useState('');
932
+ const [query, setQuery] = React.useState('');
933
+ const [results, setResults] = React.useState<string[]>([]);
934
+ React.useEffect(() => {
935
+ let current = true;
936
+ const timer = setTimeout(() => {
937
+ void search(query).then((names) => {
938
+ if (current) setResults(names);
939
+ });
940
+ }, 300);
941
+ return () => {
942
+ current = false;
943
+ clearTimeout(timer);
944
+ };
945
+ }, [query, search]);
946
+ return (
947
+ <Combobox
948
+ aria-label="Country"
949
+ value={value}
950
+ onValueChange={setValue}
951
+ query={query}
952
+ onQueryChange={setQuery}
953
+ filter={() => true}
954
+ >
955
+ {results.map((name) => (
956
+ <Combobox.Option key={name} value={name}>
957
+ {name}
958
+ </Combobox.Option>
959
+ ))}
960
+ {/* The selected option stays, hidden, while the results leave it out: its label shows. */}
961
+ {value !== '' && !results.includes(value) && (
962
+ <Combobox.Option key={value} value={value} hidden>
963
+ {value}
964
+ </Combobox.Option>
965
+ )}
966
+ </Combobox>
967
+ );
968
+ }
969
+ ```
970
+
971
+ - **`filter(option, query)`** decides which options match the typed text. It is called whenever the text is not empty, with every option that is not hidden, as a `ListboxItem` (`value`, `label`, `textValue`). The default is a case-insensitive substring of `textValue`, else `label` (TagPicker's too, since 0.11). A filter may keep options that do not contain the text; for a "Create …" entry in a TagPicker, use `allowCreate` (see [TagPicker parts and free tagging](#tagpicker-parts-and-free-tagging)). A freeform Combobox filters by its text too.
972
+ - **The query** (`query`, `defaultQuery`, `onQueryChange(query)`) is the text typed since the last commit. Combobox resets it to `''` on a commit, a close, a blur, Escape, the clear button and a form reset; TagPicker when a tag is added, on Escape with the list closed and on a form reset (a blur or a close keeps it). Turning on `disabled` or `readOnly` while text is typed reports `''`; a picker that mounts locked keeps a controlled query until it unlocks. A freeform Combobox's text is its value: it never calls `onQueryChange`, and a `query` or `defaultQuery` prop warns in development.
973
+ - The typing model is unchanged: typing never clears a Combobox selection, and a blur selects nothing. The `CustomFilter` and `AsyncSearch` stories of Combobox show both props.
974
+
975
+ ### TagPicker parts and free tagging
976
+
977
+ `<TagPicker options={…} />` is the parts with their defaults. The parts form replaces only the part you need: your own Tags in `TagPicker.Group`, rich `TagPicker.Option`s in `TagPicker.List`, or `TagPicker.Button` in place of `TagPicker.Input`. A people picker:
978
+
979
+ ```tsx
980
+ import * as React from 'react';
981
+ import { Avatar, Tag, TagPicker } from '@mortenbrudvik/waveui';
982
+
983
+ const people = [
984
+ { value: 'katri', name: 'Katri Athokas', role: 'Designer' },
985
+ { value: 'elvia', name: 'Elvia Atkins', role: 'Engineer' },
986
+ ];
987
+ const nameOf = (value: string) => people.find((person) => person.value === value)?.name ?? value;
988
+
989
+ export function PeoplePicker() {
990
+ const [value, setValue] = React.useState<string[]>(['katri']);
991
+ return (
992
+ <TagPicker aria-label="People" value={value} onValueChange={setValue}>
993
+ <TagPicker.Control>
994
+ <TagPicker.Group>
995
+ {value.map((id) => (
996
+ <Tag key={id} value={id} media={<Avatar name={nameOf(id)} decorative />} dismissLabel="Remove">
997
+ {nameOf(id)}
998
+ </Tag>
999
+ ))}
1000
+ </TagPicker.Group>
1001
+ <TagPicker.Input />
1002
+ </TagPicker.Control>
1003
+ <TagPicker.List>
1004
+ {people.map((person) => (
1005
+ <TagPicker.Option
1006
+ key={person.value}
1007
+ value={person.value}
1008
+ label={person.name}
1009
+ media={<Avatar name={person.name} decorative />}
1010
+ secondaryContent={person.role}
1011
+ >
1012
+ {person.name}
1013
+ </TagPicker.Option>
1014
+ ))}
1015
+ </TagPicker.List>
1016
+ </TagPicker>
1017
+ );
1018
+ }
1019
+ ```
1020
+
1021
+ - **Defaults.** A part without children renders its default content: `<TagPicker.Control />` holds the default tags and the Input, `<TagPicker.Group />` the default tags, `<TagPicker.List />` the options of `options`. Without a `TagPicker.List` the default list is added. An `options` entry's `media` shows in its option and its tag, its `secondaryContent` in its option.
1022
+ - **Placement.** Render `TagPicker.Control` and `TagPicker.List` directly inside the TagPicker (a Fragment is fine); a List elsewhere, or a second one, warns in development (`TagPicker.List:placement`).
1023
+ - **Selected options stay rendered.** With a List that has children, its options are the options: keep the options of the selected values in it. TagPicker hides them and reads their labels, which the tags, the summary and the announcements use. `label` sets an option's text where the children are not plain text (`TagPicker.Option:label` warns otherwise).
1024
+ - **Your own tags** follow the `value` order, render a `value` each and keep the Dismiss button: TagPicker names it with `labels.remove`, and Delete, Backspace, Enter, Space or a click on it removes the tag. With `overflow="count"`, the tags are measured by position, so keep them in `value` order.
1025
+ - **Routing.** The trigger that renders (`TagPicker.Input` or `TagPicker.Button`) receives the routed props of the root (`id`, the naming and validation ARIA attributes, `tabIndex`, `autoFocus`; the Input also the focus and key handlers, `controlRef` and the text input attributes). With `TagPicker.Button`, pass handlers and `ref` on the Button. Render exactly one trigger (`TagPicker:trigger` warns otherwise).
1026
+ - **`TagPicker.Button`** is a select-only trigger for a short, fixed list: Down, Up and a typed letter open the list and move, Enter and Space add the active option, a click toggles the list. It stays visible as the click area once tags are selected, and ignores `query`.
1027
+ - **Free tagging.** `allowCreate` ends the list with a create option (`labels.create`, default `Create "{text}"`) while the trimmed text matches no option label, no selected value (ignoring case) and no option value. Enter on it or a click calls `onOptionCreate(text)`, adds the text as a tag (`onValueChange`) and clears the text. Add an option for the new value in `onOptionCreate`, or the tag shows the raw text. It needs `TagPicker.Input` (`TagPicker:allowCreate-button` warns with the Button). With a server-side search the create option follows the current results.
1028
+ - **The exact match.** The option whose label equals the typed text (ignoring case) becomes active, else the first match; no option is active when the list opens.
1029
+ - **`clearable`** shows a clear button (`labels.clear`, "Clear selection") while a tag is selected: it removes every tag, clears the text, closes the list and focuses the trigger. It is a Tab stop after the trigger.
1030
+ - **`expandIcon`.** A chevron at the end of the field opens and closes the list (`labels.expand`, "Show options"); it is not a Tab stop and keeps focus on the trigger. `false` hides it (the 0.10 field). A press on the field's empty area toggles the list too.
1031
+ - **`overflow="count"`** keeps the tags on one line while focus is outside the picker and its list is closed; the tags that do not fit give way to "+N" (`labels.overflowCount`), and focusing the picker shows them all. Screen readers hear the trigger's summary of every tag, not the count. Fluent's single-line layout is a story; WaveUI's is a prop.
1032
+
1033
+ ```tsx
1034
+ import * as React from 'react';
1035
+ import { TagPicker, type TagPickerOption } from '@mortenbrudvik/waveui';
1036
+
1037
+ export function Topics() {
1038
+ const [options, setOptions] = React.useState<TagPickerOption[]>([
1039
+ { value: 'design', label: 'Design' },
1040
+ { value: 'testing', label: 'Testing' },
1041
+ ]);
1042
+ const [value, setValue] = React.useState<string[]>([]);
1043
+ return (
1044
+ <TagPicker
1045
+ aria-label="Topics"
1046
+ options={options}
1047
+ value={value}
1048
+ onValueChange={setValue}
1049
+ allowCreate
1050
+ // Called before onValueChange reports the new value: the new tag keeps its label.
1051
+ onOptionCreate={(text) => setOptions((current) => [...current, { value: text, label: text }])}
1052
+ clearable
1053
+ />
1054
+ );
1055
+ }
1056
+ ```
1057
+
1058
+ ### Listbox and custom pickers
1059
+
1060
+ `Listbox` is the option list of the pickers placed in the page: a `<ul role="listbox">` that holds focus itself and points `aria-activedescendant` at the active option.
1061
+
1062
+ ```tsx
1063
+ import { Field, Listbox } from '@mortenbrudvik/waveui';
1064
+
1065
+ export function FruitList() {
1066
+ return (
1067
+ <Field label="Fruit" required>
1068
+ <Listbox name="fruit" defaultValue="banana" className="max-h-48">
1069
+ <Listbox.OptionGroup label="Common">
1070
+ <Listbox.Option value="apple">Apple</Listbox.Option>
1071
+ <Listbox.Option value="banana">Banana</Listbox.Option>
1072
+ </Listbox.OptionGroup>
1073
+ <Listbox.Option value="durian" disabled>
1074
+ Durian
1075
+ </Listbox.Option>
1076
+ </Listbox>
1077
+ </Field>
1078
+ );
1079
+ }
1080
+ ```
1081
+
1082
+ - **Listbox or List.** Use `Listbox` for a choice among options (the option model of Dropdown and Combobox: typeahead, groups, disabled options, forms). Use `List` with `selectable` when each item needs real focus, actions of its own or interactive content.
1083
+ - **Focus and selection.** One Tab stop. The active option exists only while the list has focus: the selected option (with `multiselect`, the first selected one in list order), else the first; while no option can be active, the list draws its own focus ring. Space and Enter select, so the selection does not follow the active option; a single-select Listbox never deselects. A press on an option focuses the list and makes that option active without scrolling, so the click selects it even while the selected option is scrolled out of view. Tab, Escape and the Alt+arrow keys are left to the page: an enclosing Dialog closes on Escape.
1084
+ - **Props.** `multiselect` (an array value, announced toggles; see [Multi-select pickers](#multi-select-pickers)), `onActiveOptionChange` (`null` when focus leaves the list), `disabled` (out of the Tab order, selects and submits nothing), `disabledOptionsFocusable`, `autoFocus`, `name`, `form` and `required`, and Field wiring. It has no built-in height: give it one through `className` and it scrolls.
1085
+ - **Option groups.** `OptionGroup`'s `label` is optional and takes any text content (an icon next to the text is fine). Without one no heading renders, and `aria-label` or `aria-labelledby` names the group; these two always land on the `role="group"` list. A group without a name warns in development.
1086
+
1087
+ **Pickers of your own.** The pieces under Dropdown, Combobox and TagPicker are public:
1088
+
1089
+ - `useListbox(options)` holds the listbox: its keys, typeahead, filtering, the active option and the registration of the options. `mode` is `'select-only'` (a button combobox, as Dropdown), `'editable'` (an `<input>` combobox, as Combobox) or `'standalone'` (a list that holds focus, as Listbox). You own the open state (`open`, `onOpenChange(open, details)`) and the value (`selectedValues`, `onSelect(value, details)`, with `details.item` and `details.event`); spread `getComboboxProps()` on your combobox and attach `onKeyDown` and `onKeyUp`.
1090
+ - `useListboxPopup(options)` dismisses the popup (Escape, a press outside, focus leaving: `DismissReason`) and positions it, and `ListboxSurface` renders the list: in a portaled surface while it is shown, `hidden` in place otherwise, so the options register from the first render. `listClassName` and `surfaceClassName` are merged last (`max-h-*` replaces the list's height). Two duties remain yours: set your combobox's `aria-expanded` from `expanded` after spreading `getComboboxProps()`, and while the listbox is open with nothing to show, close it on Escape yourself (outside an IME composition) without calling `listbox.onKeyDown`, so the Escape reaches an enclosing layer.
1091
+ - `ListboxProvider` provides the listbox's context to its options when you render the list yourself instead of through `ListboxSurface`. `ListboxSurface`'s `listProps` (`ListboxSurfaceListProps`) pass attributes and a `ref` to the list element; the listbox's own `aria-multiselectable` and `aria-activedescendant` always win.
1092
+ - Options are `Option` and `OptionGroup` (or `TagPicker.Option`, an Option with `media` and `secondaryContent`), or components of your own on `useListboxOption`, marked with `markListboxElement` so that `collectOptionLabels(children)` knows their labels before they register (in the server HTML and the first render).
1093
+
1094
+ ```tsx
1095
+ import * as React from 'react';
1096
+ import {
1097
+ ListboxSurface,
1098
+ Option,
1099
+ collectOptionLabels,
1100
+ useListbox,
1101
+ useListboxPopup,
1102
+ useMergedRefs,
1103
+ } from '@mortenbrudvik/waveui';
1104
+
1105
+ const SIZES = (
1106
+ <>
1107
+ <Option value="s">Small</Option>
1108
+ <Option value="m">Medium</Option>
1109
+ <Option value="l">Large</Option>
1110
+ </>
1111
+ );
1112
+
1113
+ export function SizePicker() {
1114
+ const [open, setOpen] = React.useState(false);
1115
+ const [value, setValue] = React.useState('m');
1116
+ const rootRef = React.useRef<HTMLDivElement>(null);
1117
+ const buttonRef = React.useRef<HTMLButtonElement>(null);
1118
+ const listbox = useListbox({
1119
+ open,
1120
+ onOpenChange: setOpen,
1121
+ mode: 'select-only',
1122
+ selectedValues: [value],
1123
+ // A single-select commit also closes the list, through onOpenChange.
1124
+ onSelect: setValue,
1125
+ });
1126
+ const expanded = open && listbox.items.length > 0;
1127
+ const { layerId, setReference, surfaceRef, floatingProps } = useListboxPopup({
1128
+ open,
1129
+ surfaceOpen: expanded,
1130
+ onDismiss: () => setOpen(false),
1131
+ rootRef,
1132
+ anchorRef: buttonRef,
1133
+ });
1134
+ const buttonMergedRef = useMergedRefs(buttonRef, setReference);
1135
+ return (
1136
+ <div ref={rootRef}>
1137
+ <button
1138
+ type="button"
1139
+ aria-label="Size"
1140
+ {...listbox.getComboboxProps()}
1141
+ aria-expanded={expanded}
1142
+ ref={buttonMergedRef}
1143
+ onClick={() => setOpen(!open)}
1144
+ onKeyDown={listbox.onKeyDown}
1145
+ onKeyUp={listbox.onKeyUp}
1146
+ >
1147
+ {listbox.getItem(value)?.label ?? collectOptionLabels(SIZES).get(value)}
1148
+ </button>
1149
+ <ListboxSurface
1150
+ listbox={listbox}
1151
+ layerId={layerId}
1152
+ surfaceRef={surfaceRef}
1153
+ floatingProps={floatingProps}
1154
+ open={open}
1155
+ expanded={expanded}
1156
+ aria-label="Sizes"
1157
+ >
1158
+ {SIZES}
1159
+ </ListboxSurface>
1160
+ </div>
1161
+ );
1162
+ }
1163
+ ```
1164
+
1165
+ The `CustomPicker` story in `stories/Listbox.stories.tsx` is an editable font picker built only on these exports, and `CommandPalette` uses `useActiveDescendant` alone, for a composite that is not a listbox. The guide's [Building a custom picker](docs/WAVE-UI-GUIDE.md#pattern-12-building-a-custom-picker) adds option components of your own, a list rendered with `ListboxProvider`, and [active-descendant widgets](docs/WAVE-UI-GUIDE.md#pattern-13-active-descendant-widgets).
1166
+
856
1167
  ### Color
857
1168
 
858
1169
  ```tsx
@@ -881,6 +1192,83 @@ export function AccentPicker() {
881
1192
  - **Assistive technology.** The area is a group of two sliders, "Saturation" and "Brightness": desktop screen readers list one 2-D slider, and touch screen readers can swipe each. Each value text starts with its own channel and names the color ("Saturation 72%, Brightness 74%, dark blue"); a hue slider says "210°, blue". `labels.colorName(color)` replaces the English color names (return `''` to leave them out), and `labels.valueText(color, channel)` a whole value text, for languages whose word order differs.
882
1193
  - `ColorSlider` takes `channel` (`'hue'`, `'saturation'` or `'value'`) and `orientation` (`'vertical'` draws the rail bottom to top); `AlphaSlider` shows opacity, or with `transparency` transparency (the value's `a` is still the opacity). The roots carry `data-orientation`, `data-channel` (ColorSlider) and, while disabled, `data-disabled`. `shape="square"` removes the corner radius. Inside a `Field`, a standalone slider is named by the Field's label, else by its `labels` ("Hue", "Saturation", "Brightness").
883
1194
 
1195
+ ### Swatches
1196
+
1197
+ ```tsx
1198
+ import {
1199
+ ColorSwatch,
1200
+ EmptySwatch,
1201
+ ImageSwatch,
1202
+ SwatchPicker,
1203
+ Tooltip,
1204
+ } from '@mortenbrudvik/waveui';
1205
+
1206
+ export function AccentSwatches({ onAddColor }: { onAddColor: () => void }) {
1207
+ return (
1208
+ <SwatchPicker layout="grid" aria-label="Accent" name="accent" defaultValue="red">
1209
+ <SwatchPicker.Row>
1210
+ <Tooltip content="Red" relationship="label">
1211
+ <ColorSwatch value="red" color="#d13438" />
1212
+ </Tooltip>
1213
+ <ColorSwatch value="blue" color="#0f6cbd" aria-label="Blue" />
1214
+ <ColorSwatch value="sand" color="#faf9f8" borderColor="#8a8886" aria-label="Sand" />
1215
+ </SwatchPicker.Row>
1216
+ <SwatchPicker.Row>
1217
+ <ImageSwatch value="dots" src="/patterns/dots.png" aria-label="Dots" />
1218
+ <ColorSwatch value="green" color="#107c10" aria-label="Green" disabled />
1219
+ <EmptySwatch onClick={onAddColor} />
1220
+ </SwatchPicker.Row>
1221
+ </SwatchPicker>
1222
+ );
1223
+ }
1224
+ ```
1225
+
1226
+ - **Swatches as children.** `ColorSwatch`, `ImageSwatch` and `EmptySwatch` read the picker's context (one outside a SwatchPicker throws in development), so a swatch can sit in a `Tooltip`: its `ref`, `className`, `style`, name and other props go to its own `<button>` in every layout, and a Tooltip with `relationship="label"` names it. `items` is optional and renders `ColorSwatch`es before the children. Swatch values are unique (a duplicate warns in development).
1227
+ - **Kinds.** A `ColorSwatch` (`value`, `color`) is named by `aria-label` or a Tooltip, else by its color value (with a development warning); `icon` is a decorative glyph shown while it is not selected, and `borderColor` sets its border for a color close to the page background (forced colors replace it). An `ImageSwatch` (`value`, `src`) draws a pattern or texture as a covering background. `disabled` is native (out of the keys and the Tab order, never selected): the swatch is dimmed and crossed by a diagonal slash, so the state does not rely on its color, and a disabled swatch that is selected keeps its selected look. An `EmptySwatch` is an "add" button with no value, named "Add color" by default (`aria-label`), with a plus `icon` (`false` hides it) and your `onClick`. Every swatch takes `size` and `shape`, which override the picker's.
1228
+ - **Row layout** (the default): a `role="radiogroup"` of `role="radio"` buttons whose arrow keys select and wrap, as in 0.9. An `EmptySwatch` there is a plain button with its own Tab stop, outside the arrow keys.
1229
+ - **Grid layout** (`layout="grid"`): a `role="grid"` of `SwatchPicker.Row` rows (flat `SwatchPickerRow`), each swatch button inside a `role="gridcell"`. `items` with the children that follow them up to a row, and every other run of children that are not rows, form implicit rows, so a Row must be a direct child of the picker or inside a Fragment (one inside a component of yours lands in an implicit row). Rows do not wrap, so size them yourself. The arrow keys move focus only, in two dimensions; Space, Enter and a click select. The selected swatch's cell is `aria-selected` and its button `aria-pressed`. An `EmptySwatch` is a cell in the arrow order. Changing `layout` on a mounted picker remounts the swatches.
1230
+ - **Tab mode** (`focusMode="tab"`, in either layout): every enabled swatch is its own Tab stop and the arrow keys do nothing, so the picker is a `role="group"` of toggle buttons (`aria-pressed`); rows have no role. Pressing the selected swatch keeps it selected. Fluent keeps the radio and grid roles there.
1231
+ - **Sizes.** `size` is `'extra-small'` (20px), `'small'` (24px), `'medium'` (32px, the default) or `'large'` (40px), and `spacing` is `'small'` (4px) or `'medium'` (8px, the default), between swatches and between rows; a 20px swatch meets WCAG 2.5.8 through its spacing. `shape` is `'circular'` by default.
1232
+ - **Required.** The row layout carries `aria-required`. A grid or a group cannot, so the grid layout and tab mode say "Required" in the picker's description (`labels.required`).
1233
+ - The root carries `data-layout`, `data-focus-mode`, `data-size`, `data-shape` and `data-spacing`. `ColorPicker.Swatches` takes the same props, so a ColorPicker's presets can use the grid layout or end in an `EmptySwatch`.
1234
+
1235
+ ### Calendar
1236
+
1237
+ ```tsx
1238
+ import * as React from 'react';
1239
+ import { Calendar, Field } from '@mortenbrudvik/waveui';
1240
+
1241
+ export function SprintWeek({ today }: { today: Date }) {
1242
+ const [days, setDays] = React.useState<Date[]>([]);
1243
+ return (
1244
+ <Field label="Sprint week" hint={`${days.length} days selected`} required>
1245
+ <Calendar
1246
+ name="sprint"
1247
+ locale="en-GB"
1248
+ today={today}
1249
+ firstDayOfWeek={1}
1250
+ showWeekNumbers
1251
+ firstWeekOfYear="first-four-day-week"
1252
+ selectionRange="work-week"
1253
+ showGoToToday
1254
+ markedDates={(date) => date.getDay() === 5}
1255
+ onValueChange={(_date, details) => setDays(details?.range ?? [])}
1256
+ />
1257
+ </Field>
1258
+ );
1259
+ }
1260
+ ```
1261
+
1262
+ - **Value.** `value` and `defaultValue` take a `Date` or `null`; `onValueChange(date, details)` fires only when the day changes, with local midnight, and `details.range` lists the selected days (Fluent's `onSelectDate` fires on every pick). Picking the selected day again does nothing. A controlled value in another month shows that month. `minDate`, `maxDate`, `disabledDates`, `firstDayOfWeek` and `locale` work as in DatePicker.
1263
+ - **Month and year views** (`showMonthPicker`, on by default): the month text is a button, named by that text and described by `labels.chooseMonth`, that shows the 12 months of the year; their heading, the year, is a button to the year view (`labels.chooseYear`): a decade with the year before and after it. The header buttons move by a month, a year or a decade. A picked year shows its months and a picked month its days (the focused day keeps its day of the month, clamped), and Escape goes back a view to the period shown before. Every view change moves focus into the new grid. Months and years wholly outside `minDate`/`maxDate` stay focusable and `aria-disabled`. `showMonthPicker={false}` keeps a static month text.
1264
+ - **Week numbers.** `showWeekNumbers` adds a leading column of row headers ("Week 23", `labels.weekNumber`), which the keys skip. `firstWeekOfYear` is `'first-day'` (the default), `'first-full-week'` or `'first-four-day-week'`; ISO 8601 weeks are `'first-four-day-week'` with `firstDayOfWeek={1}`.
1265
+ - **Ranges** (Calendar only). `selectionRange` is `'day'` (the default), `'week'`, `'work-week'` or `'month'`, and `workWeekDays` the days of a work week (Monday to Friday by default). A pick selects the picked day's week, work week or month without its unavailable days: the value is the picked day, `details.range` the selected days, and every selected day is `aria-selected`, `aria-pressed` and `data-selected`.
1266
+ - **Marked days and custom content.** `markedDates(date)` draws a dot under the number, sets `data-marked` and adds "marked" to the day's name (`labels.markedDay`). `renderDay(date, defaultContent)` replaces the content of a day button (non-interactive content only), and `dayLabel(date, defaultLabel)` sets its name, since the content does not.
1267
+ - **Footer.** `showGoToToday` adds a button that shows today's month and focuses today without selecting it (disabled while today is outside `minDate`/`maxDate`). DatePicker's `showCloseButton` adds a Close button that closes the calendar as Escape does. Both are off by default.
1268
+ - **Server rendering and `today`.** `today` (the current date by default) is the day marked `aria-current="date"`. Without it, the server HTML marks no day and the browser marks today once hydrated. The month shown first is the value's, else `initialVisibleDate`'s, else today's, clamped into `minDate`/`maxDate`: on the server, pass `locale` and one of `today`, `initialVisibleDate` or a value, since without them the month comes from the server's clock (a development warning says so).
1269
+ - **Forms and Field.** `name`, `form` and `required` add a hidden input (ISO `yyyy-mm-dd`) that resets with its form to `defaultValue`; when the form reports a required Calendar empty, its tab stop takes focus. Inside a `Field` the `role="group"` root takes the label, the description and `aria-invalid`, and says "Required" in its description (`labels.required`), since a group cannot carry `aria-required`. The month text is a `<div>` that labels the grid (DatePicker's dialog keeps its `<h2>`), since a component cannot know the page's heading levels.
1270
+ - **DatePicker** shows the same calendar in its dialog, with the props above except `selectionRange` and `workWeekDays` (its input holds one date). The date helpers it uses are public: see [Hooks and utilities](#hooks-and-utilities).
1271
+
884
1272
  ### Menus
885
1273
 
886
1274
  ```tsx
@@ -1098,14 +1486,20 @@ export function DeleteDialog({ onDelete }: { onDelete: () => void }) {
1098
1486
  }
1099
1487
  ```
1100
1488
 
1489
+ - **Triggers toggle.** `Dialog.Trigger` and `Drawer.Trigger` open their surface and, clicked while it is open, close it: a trigger inside its own surface, and the trigger of a non-modal or inline surface, which stays reachable. The trigger of an inline drawer has no `aria-haspopup` (a render-prop trigger reads it as `undefined`).
1101
1490
  - `Dialog.Trigger`, `Drawer.Trigger`, `Popover.Trigger` and `Menu.Trigger` merge their props onto their single child (no wrapper element; a Fragment around one element counts as that element) or pass them to a render-prop child. `asChild={false}` renders the 0.4 wrapper `<span>`; a custom child that neither forwards `ref` nor spreads its props, and text or several children, fall back to that span automatically, with a development warning. The span keeps the trigger's other props, but `aria-haspopup`, `aria-expanded` and `aria-controls` go to the first element in the tab order inside it (after mount), so a button there is announced as opening the popup, and Menu, Popover, Dialog and Drawer return focus to that element (to the span only when nothing inside it can take focus). A span you made the trigger itself, with a `role` such as `button` and `tabIndex={0}`, keeps them and takes focus back; `tabIndex` alone leaves the span generic, so it never carries them. With `openOnContext` (Menu, Popover) the child is a context-menu region, not a popup button: it receives your forwarded props and, of the trigger's own, only its id, ref and context-gesture handlers (no state ARIA, no click toggle).
1102
- - Render `Dialog.Footer` **inside** `Dialog.Content`, one per content. It stays in view: it sticks to the bottom of the scrolling body with an opaque background, and the body reserves its height as scroll padding, so a focused field is never hidden behind it. It keeps its place in the DOM; inside a `<form>` that wraps the fields and the footer, make it the form's last child. Inside a Drawer, `Dialog.Footer` does not stick: it is an action row at the end of the content.
1103
- - Dialog and Drawer trap focus, close on Escape and a backdrop press, lock page scroll and make the rest of the page `inert` while open (toasts and live regions stay available). A backdrop press blurs the focused field before the surface closes, so a typed value (a SpinButton's, a picker's) is committed as with the Close button. When the press does not close the surface (an alert dialog, or a controlled one that refuses `outside-press`), focus ends up where it was.
1491
+ - Render `Dialog.Footer` **inside** `Dialog.Content`, one per content. It stays in view: it sticks to the bottom of the scrolling body with an opaque background, and the body reserves its height as scroll padding, so a focused field is never hidden behind it. It keeps its place in the DOM; inside a `<form>` that wraps the fields and the footer, make it the form's last child. Inside a Drawer, use `Drawer.Footer` (see [Drawers](#drawers)); a `Dialog.Footer` there does not stick, it is an action row at the end of the content.
1492
+ - A modal or alert Dialog or Drawer traps focus, closes on Escape and a backdrop press, locks page scroll and makes the rest of the page `inert` while open (toasts and live regions stay available). A backdrop press blurs the focused field before the surface closes, so a typed value (a SpinButton's, a picker's) is committed as with the Close button. When the press does not close the surface (an alert dialog, or a controlled one that refuses `outside-press`), focus ends up where it was.
1104
1493
  - **Alert dialogs.** `modalType="alert"` is for a confirmation that needs an answer: the content is `role="alertdialog"`, and a backdrop press does not close it (Escape, the Close button and `Dialog.Close` still do). Initial focus goes to the first focusable element, the built-in Close button; the order of your buttons does not change that, so give the least destructive action `autoFocus` to start there, as above.
1105
1494
  - **Why it opens or closes.** `onOpenChange(open, details)` gets `details.reason` (`'trigger'`, `'close'` for a `.Close` part, `'close-button'` for the built-in Close button, `'escape'`, `'outside-press'` for the backdrop) and `details.event`, the DOM event. A controlled dialog or drawer can refuse some of them (below). `details` is typed optional until 1.0, when it becomes required: read it as `details?.reason`. A render-prop trigger or close part that calls `onClick()` without its event reports a new `click` event with no target, so pass the event on.
1106
1495
  - On close, focus goes to `finalFocusRef` when you pass one. Otherwise it returns to the element that had focus when the modal opened (a trigger's wrapper span resolves to the element inside it, see the trigger bullet above): the trigger for a trigger click, but also the parent's own button of a controlled Dialog, or a text field that had focus when a keyboard shortcut opened it. When nothing had focus (a modal opened from code, or a click in Safari, which does not focus the clicked button), it goes to the trigger that opened the modal, else the first mounted trigger. When that element is gone, focus falls back to a mounted trigger, then to an element next to where the opener was (the next row's action after a delete), then to the overlay below.
1107
- - An `autoFocus` element inside a Dialog or Drawer keeps focus, also in a dialog opened over another one or nested in its content, and in a `Popover.Content` opened from a dialog. `Dialog.Content` and `Drawer` take `closeLabel` (default `'Close'`) to localize the Close button's name.
1108
- - **Size.** `size` caps the dialog's width at 400px (`'small'`) or 600px (`'medium'`, the default). Below the cap it takes the full width, and it never exceeds the viewport: a 1rem margin keeps it off the viewport edges, its height is capped, and the body scrolls (sideways too, for content wider than the dialog). Because the margin and the caps are on the surface, a `className` on `Dialog.Content` replaces them: `max-w-[72rem]` for a wider dialog, `m-0 h-dvh max-h-none max-w-none rounded-none border-0` for a full-screen one, and the same classes behind a breakpoint (`max-[48rem]:m-0 max-[48rem]:h-dvh …`) for a dialog that goes full screen only on narrow screens. These are Tailwind classes for your own Tailwind build. The precompiled `./styles` contains only the utilities Wave uses: there, give `Dialog.Content` a class of your own that sets the same properties, in unlayered CSS loaded after Wave's, because Wave's classes are unlayered and as specific as yours ([Cascade](#styles)).
1496
+ - An `autoFocus` element inside a Dialog or Drawer keeps focus (not in a surface kept mounted with `unmountOnClose={false}`, where each open focuses the first focusable element), also in a dialog opened over another one or nested in its content, and in a `Popover.Content` opened from a dialog. `Dialog.Content` and `Drawer` take `closeLabel` (default `'Close'`) to localize the Close button's name.
1497
+ - **Size.** `size` caps the dialog's width at 400px (`'small'`), 600px (`'medium'`, the default) or 940px (`'large'`, Drawer's `large`). Below the cap it takes the full width, and it never exceeds the viewport: a 1rem margin keeps it off the viewport edges, its height is capped, and the body scrolls (sideways too, for content wider than the dialog). `'full'` covers the viewport: no margin, radius or border, the viewport's dynamic height, the safe-area insets as padding (with `viewport-fit=cover`), and the body scrolls under the sticky `Dialog.Footer`. `data-size` renders the size. For another width, a width cap in `className` replaces the size's: `max-w-[72rem]`, or the classes behind a breakpoint (`max-[48rem]:m-0 max-[48rem]:h-dvh …`) for a dialog that goes full screen only on narrow screens. These are Tailwind classes for your own Tailwind build. The precompiled `./styles` contains only the utilities Wave uses: there, give `Dialog.Content` a class of your own that sets the same properties, in unlayered CSS loaded after Wave's, because Wave's classes are unlayered and as specific as yours ([Cascade](#styles)).
1498
+ - **Non-modal.** `modalType="non-modal"` (Dialog and Drawer) leaves the page usable: no backdrop, no focus trap, nothing `inert`, the page scrolls. Focus moves into the surface on open; Tab and Shift+Tab leave it in the page's order right after the element that opened it; Escape closes it only while focus is inside it; presses on the page never close it; and focus returns to the opener only when it was inside. The surface covers the page under it while Tab still reaches that page, so keep the page's controls clear of it: lay them out beside a centred dialog, reserve a non-modal drawer's width (its height at the bottom) in your layout while it is open, or use an inline drawer (`type="inline"`) for a panel beside the content. With `size="full"` a non-modal surface hides the whole page, and a development warning says so.
1499
+ - **Backdrop.** The backdrop is the surface's sibling, `aria-hidden` (`.wave-dialog-content__backdrop`, `.wave-drawer__backdrop`), and a slot on `Dialog.Content` and Drawer: content renders inside it, a slot object's `className`, `style` and handlers go to it, and its `appearance` (`'dimmed'` or `'transparent'`, Fluent's name) picks the fill. A modal opened while another modal Dialog or Drawer is open below it gets a transparent backdrop, so the page dims once; `backdrop={{ appearance: 'dimmed' }}` dims it again. `null` renders no backdrop (the page stays inert, and a press outside still closes a modal); a boolean is no slot. A non-modal surface or an inline drawer ignores `backdrop`, with a development warning.
1500
+ - **Close button.** `closeButton` sets the content of the built-in Close button (decorative: the button keeps `closeLabel` as its name, unless a slot object's `aria-label`, `aria-labelledby` or `title` names it); `null` hides it (Escape and `.Close` parts still close). A `<button>` or `Button` passed there is merged into the built-in button, never nested, and its text label names it. Fluent's dialogs have no Close button by default: pass `closeButton={null}` and keep yours, or drop yours.
1501
+ - **Title actions.** `Dialog.Content`'s `titleAction` (for the `title` prop) and `Dialog.Title`'s `action` place header buttons before the Close button, in the top end corner, outside the heading, so they never join the dialog's name; the title's end padding follows their width. Drawer's `titleAction` and `Drawer.HeaderTitle`'s `action` do the same in a drawer header. Name icon-only buttons.
1502
+ - **Kept mounted.** `unmountOnClose={false}` keeps `Dialog.Content` mounted while closed, `hidden` and `inert`, so a form keeps what was typed; a popup opened inside that is still open when the dialog closes stays open over the page (a development warning says so). Drawers take the same prop. See [Enter and exit motion](#enter-and-exit-motion) for the `data-presence` phases both now carry, and [Drawers](#drawers) for side and bottom panels.
1109
1503
 
1110
1504
  ```tsx
1111
1505
  import * as React from 'react';
@@ -1140,6 +1534,49 @@ export function RenameDialog() {
1140
1534
  }
1141
1535
  ```
1142
1536
 
1537
+ ### Drawers
1538
+
1539
+ A Drawer is a side or bottom panel, by default a modal overlay (see [Dialogs and triggers](#dialogs-and-triggers) for the triggers, closing and focus):
1540
+
1541
+ ```tsx
1542
+ import { Button, Drawer, Field, Input } from '@mortenbrudvik/waveui';
1543
+
1544
+ export function FiltersDrawer() {
1545
+ return (
1546
+ <Drawer size="medium">
1547
+ <Drawer.Trigger>
1548
+ <Button>Filters</Button>
1549
+ </Drawer.Trigger>
1550
+ <Drawer.Header>
1551
+ <Drawer.HeaderTitle>Filters</Drawer.HeaderTitle>
1552
+ </Drawer.Header>
1553
+ <form id="filters" onSubmit={(event) => event.preventDefault()}>
1554
+ <Field label="Keyword">
1555
+ <Input name="keyword" />
1556
+ </Field>
1557
+ </form>
1558
+ <Drawer.Footer>
1559
+ <Button appearance="primary" type="submit" form="filters">
1560
+ Apply
1561
+ </Button>
1562
+ <Drawer.Close>
1563
+ <Button>Cancel</Button>
1564
+ </Drawer.Close>
1565
+ </Drawer.Footer>
1566
+ </Drawer>
1567
+ );
1568
+ }
1569
+ ```
1570
+
1571
+ - **Position and size.** `position` is `end` (the default) or `start`, which follow the text direction, `left` or `right`, or `bottom` (along the bottom edge). `size` sets the width at a side and the height at the bottom: `small` (320px, the default), `medium` (592px), `large` (940px) or `full`; the panel never grows past the window. A width class in `className` (a height class for `bottom`) replaces the size. Both render as `data-position` and `data-size`. An overlay panel pads the safe-area insets of the screen edges it touches, once the page sets `viewport-fit=cover`.
1572
+ - **Header, body and footer.** The `title` prop and the built-in Close button make the header row. A `Drawer.Header` replaces it: a `Drawer.HeaderNavigation` row (a Back button, a Toolbar of commands, in a `<nav>`) and a `Drawer.HeaderTitle` (the heading, `headingLevel`, an `action` area and the built-in Close button, which `closeButton={null}` hides). The rest of the content scrolls in the body; `Drawer.Body` gives it your props. `Drawer.Footer` is an action row below the body that never scrolls, wherever it is written among the children, with its actions at the start (Fluent's DrawerFooter; `justify-end` puts them at the end). Make the parts direct children of the Drawer (a Fragment there is fine): a part nested in another element renders in the body, with a development warning.
1573
+ - **Separators.** The header draws a line under itself once the body is scrolled, the footer one above itself while content continues below (always in forced colours). The body, the header and the footer render the body's scroll state as `data-scroll-state` (`none`, `top`, `middle`, `bottom`) for your own styles.
1574
+ - **Forms.** The footer is outside the body, so a `<form>` in the body does not contain it: give the form an `id` and its submit button `form`, as above.
1575
+ - **Non-modal.** `modalType="non-modal"` leaves the page usable (see [Dialogs and triggers](#dialogs-and-triggers)); reserve the panel's width (its height at the bottom) in your layout while it is open, so it hides no page control. `modalType="alert"` renders `role="alertdialog"`.
1576
+ - **Inline.** `type="inline"` renders the panel where the Drawer is, after its direct triggers, as an `<aside>` (a complementary landmark named by its title), also in the server HTML. It is page content: no portal, backdrop, focus trap, inert page or scroll lock, Escape and presses outside never close it, and toasts keep their corner. Its trigger toggles it; opening it moves no focus, and closing it returns focus only when focus was inside. `position` picks its inner edge and `size` its width; `separator` draws the inner edge. Place it in your layout, for example a flex row of the panel and the content. Changing `type` (an overlay below a breakpoint, inline above it) remounts the panel and its content.
1577
+ - **Kept mounted.** `unmountOnClose={false}` keeps the panel and its content mounted while closed, `hidden` and `inert`, so a form keeps what was typed; an `autoFocus` inside never applies (each open of an overlay focuses its first focusable element), and a kept inline panel is in the server HTML, hidden. A `required` empty field in a closed kept drawer inside a page `<form>` still blocks its submission.
1578
+ - **Motion.** The panel and its backdrop mount through the presence core and render `data-presence` (see [Enter and exit motion](#enter-and-exit-motion)): `data-[presence=…]:` classes in `className` animate the panel, and a closing drawer waits for that motion before it unmounts or hides. No motion ships.
1579
+
1143
1580
  ### Popovers and tooltips
1144
1581
 
1145
1582
  ```tsx
@@ -1219,7 +1656,7 @@ export function SavedNotice() {
1219
1656
  - **Gate the enter on the phase.** `@starting-style` applies to every first style of an element, whatever its phase. An ungated `starting:` class, or a `@starting-style` rule without `[data-presence='entering']`, therefore also fades in an element that mounts `entered`: without `appear`, and server-rendered content after hydration, while the core already reports `entered` and has called `onEntered`. The gated form runs only when the core runs the enter phase: a show after mount, `appear`, and a kept-mounted element shown again.
1220
1657
  - **The hook.** `const { isMounted, ref, presenceProps } = usePresence(open)` for your own components: render the element while `isMounted`, pass it `ref` and spread `presenceProps` (`data-presence`, and `inert`/`hidden` when they apply). Destructure the result: `eslint-plugin-react-hooks` treats the whole object as a ref once its `ref` is passed on, and reports reads of its other members during render. Keep focus, dismissal and positioning on your open state, not on `isMounted`, so they happen on close and not after the exit motion.
1221
1658
  - **Browser support.** `starting:` needs `@starting-style` (Chrome and Edge 117+, Safari 17.5+, Firefox 129+); older browsers show the element at once, without its enter motion, and the exit still runs.
1222
- - `Menu.Popover` and the toasts in a `Toaster` mount through the presence core, so menus and toasts take exit classes too (see [Menus](#menus) and [Toasts](#toasts)). An enter class for them takes the same gate; a menu that is open when it mounts (`defaultOpen`) mounts `entered`, so it does not animate that first time. The other overlays get their motion in a later release.
1659
+ - `Menu.Popover`, the toasts in a `Toaster`, and since 0.11 `Dialog.Content` and Drawer (the panel and its backdrop, overlay or inline, NavDrawer's too) mount through the presence core, so they take exit classes too (see [Menus](#menus) and [Toasts](#toasts)). An enter class for them takes the same gate; a surface that is open when it mounts (`defaultOpen`) mounts `entered`, so it does not animate that first time. A closing Dialog or Drawer waits for its motion before it unmounts, or hides with `unmountOnClose={false}` (`exited`, `hidden` and `inert`); focus, the trap and the isolation follow the open state at once. Every top-level element of their portal carries the presence attributes. No component ships enter or exit classes yet; Popover and Tooltip get their motion in a later release.
1223
1660
 
1224
1661
  ### Toasts
1225
1662
 
@@ -1276,7 +1713,7 @@ export function App({ undoSend }: { undoSend: () => void }) {
1276
1713
  - **What is announced.** The Toaster reads each toast once, through permanent live regions that need no provider: the severity, the title, the body with its subtitle, then the labels of its actions ("Success: Email sent Your message was delivered. To 3 people Undo"), never the Dismiss button. Hidden text is skipped, and an image's `alt` is read. `announcement` in the options replaces everything but the severity, for a toast whose visible text is no good announcement (a progress toast). Dispatching the same `toastId` again replaces the toast and announces it again.
1277
1714
  - **Timing.** A toast times out after 5 seconds (`timeout` in the options). Every toast pauses while the pointer or focus is anywhere in the toast region, and while the page is in the background (the window has lost focus, or the tab is hidden, also when the Toaster mounted there). A toast with an action (a button, a link or a field besides its Dismiss button) stays until it is dismissed: people who use a screen reader, a switch or voice control need time to reach it. Pass `timeout` to change that deliberately.
1278
1715
  - **Keyboard.** `shortcuts.focus` returns `true` for the key press that moves focus to the newest toast; use a modifier (or let people change the shortcut), since a single-character shortcut fails WCAG 2.1.4. A focused toast is announced by its severity and title. Tab and Shift+Tab move through the toasts' controls; Escape dismisses every toast and returns focus to where it was (a tooltip or menu opened from a toast closes first); Delete or Backspace dismisses the focused toast (the keys are ignored in editable fields), and focus moves to the next toast, or the previous one, else back to where it was before the toasts. An Escape in the toasts never closes a Dialog below them: the next Escape does, once focus is back in the dialog.
1279
- - Toasts stay reachable by Tab over an open Dialog and move out of an open Drawer's way. `statusLabel` and `dismissLabel` (props of `<Toast>`; `statusLabel` is also an option of both `dispatchToast` forms, `dismissLabel` of the text form) translate the hidden status text and the Dismiss button's name.
1716
+ - Toasts stay reachable by Tab over an open Dialog and move out of an open overlay Drawer's way, modal or not: beside a side drawer at their corner, above a bottom one (below it for top toasts; `--wave-toaster-offset-block`), and they keep their corner when there is no room. Next to an inline drawer, which is page content, they keep their corner. `statusLabel` and `dismissLabel` (props of `<Toast>`; `statusLabel` is also an option of both `dispatchToast` forms, `dismissLabel` of the text form) translate the hidden status text and the Dismiss button's name.
1280
1717
  - **`limit`** caps how many toasts show at once (`<Toaster limit={3}>`). Further toasts wait in a queue, in dispatch order, and appear as shown toasts go; a waiting toast is not announced and its timer does not run until it shows. A dismissed toast stops counting at once, also while an exit motion of yours runs. Lowering the limit never hides a shown toast. A value below 1 counts as 1 (with a development warning), and a fraction rounds down. Dispatching the id of a waiting toast again replaces it and keeps its place; `dismissToast(id)` removes a waiting toast silently.
1281
1718
  - **`dismissAllToasts()`** removes every toast, shown and waiting, and cancels their timers. When focus was in a toast, it returns to the element that had it before.
1282
1719
  - **Motion.** Each toast mounts through the presence core inside a container with the class `wave-toaster__toast`: style its `data-presence` phases for an enter and exit motion of yours (see [Enter and exit motion](#enter-and-exit-motion)). A dismissed toast stays mounted and `inert` until the exit ends; focus has already moved on. Wave ships no toast motion.
@@ -1342,51 +1779,31 @@ export function UpdateNotice({ onUpdate, onLater }: { onUpdate: () => void; onLa
1342
1779
 
1343
1780
  ### Tags
1344
1781
 
1345
- A tag cannot keep focus once you remove it. When `onDismiss` removes a tag, first move focus to the next tag's dismiss button, else the previous one, else a nearby control (a "Reset filters" or "Add" button), and then remove the tag: the neighbour is still mounted, so focus never falls to the page. TagGroup (planned) will do this for you. The `FilterGroup` story in `stories/Tag.stories.tsx` is the reference:
1782
+ Put tags that belong together in a `TagGroup`. By default it is a toolbar: one Tab stop, Left/Right between the tags (mirrored in RTL), Delete or Backspace to dismiss. When the focused tag is removed, focus moves to the next tag, else the previous one, else `fallbackFocusRef`, so it never falls to the page:
1346
1783
 
1347
1784
  ```tsx
1348
1785
  import * as React from 'react';
1349
- import { Button, Tag } from '@mortenbrudvik/waveui';
1786
+ import { Button, Tag, TagGroup } from '@mortenbrudvik/waveui';
1350
1787
 
1351
1788
  const ALL_FILTERS = ['Red', 'Blue', 'Large'];
1352
1789
 
1353
1790
  export function Filters() {
1354
1791
  const [filters, setFilters] = React.useState(ALL_FILTERS);
1355
- const tags = React.useRef(new Map<string, HTMLElement>());
1356
1792
  const reset = React.useRef<HTMLButtonElement>(null);
1357
-
1358
- const dismiss = (filter: string) => {
1359
- const index = filters.indexOf(filter);
1360
- const neighbour = filters[index + 1] ?? filters[index - 1];
1361
- // The dismiss button is the tag's only button.
1362
- const target =
1363
- neighbour === undefined
1364
- ? reset.current
1365
- : tags.current.get(neighbour)?.querySelector('button');
1366
- target?.focus();
1367
- setFilters((current) => current.filter((f) => f !== filter));
1368
- };
1369
-
1370
1793
  return (
1371
1794
  <div>
1372
- <div role="group" aria-label="Active filters">
1795
+ <TagGroup
1796
+ aria-label="Active filters"
1797
+ dismissible
1798
+ fallbackFocusRef={reset}
1799
+ onDismiss={(value) => setFilters((current) => current.filter((f) => f !== value))}
1800
+ >
1373
1801
  {filters.map((filter) => (
1374
- <Tag
1375
- key={filter}
1376
- dismissible
1377
- dismissLabel="Remove"
1378
- ref={(element) => {
1379
- if (element) tags.current.set(filter, element);
1380
- return () => {
1381
- tags.current.delete(filter);
1382
- };
1383
- }}
1384
- onDismiss={() => dismiss(filter)}
1385
- >
1802
+ <Tag key={filter} value={filter} dismissLabel="Remove">
1386
1803
  {filter}
1387
1804
  </Tag>
1388
1805
  ))}
1389
- </div>
1806
+ </TagGroup>
1390
1807
  {/* Nothing to reset while every filter is set: unavailable, but still focusable. */}
1391
1808
  <Button
1392
1809
  ref={reset}
@@ -1400,6 +1817,33 @@ export function Filters() {
1400
1817
  }
1401
1818
  ```
1402
1819
 
1820
+ - **Values.** Give every tag of a group its own `value`: it is what `onDismiss(value)` and the selection report. The group's `size`, `appearance`, `shape`, `dismissible` and `disabled` are the defaults of its tags (a tag's own wins, except that a disabled group disables every tag). `onDismiss` is called on every dismissal, after the Tag's own `onDismiss`; a dismissal you refuse (the tag stays) moves no focus.
1821
+ - **Static tags.** `role="list"` makes the group a list of tags that only display: every tag is a list item, every control its own Tab stop, and the group handles no arrow key.
1822
+ - **Selectable chips.** With `value`, `defaultValue` or `onValueChange` the group selects through `InteractionTag`s: each `InteractionTag.Primary` is a toggle (`aria-pressed`), and `onValueChange(values)` reports the selected values in selection order. Dismissing a selected tag also deselects it, and `name`, `form` and `required` submit one hidden input per value. Plain Tags are never selected; a Tag's `selected` is only a look.
1823
+
1824
+ ```tsx
1825
+ import * as React from 'react';
1826
+ import { InteractionTag, TagGroup } from '@mortenbrudvik/waveui';
1827
+
1828
+ export function Topics() {
1829
+ const [topics, setTopics] = React.useState<string[]>(['design']);
1830
+ return (
1831
+ <TagGroup aria-label="Topics" value={topics} onValueChange={setTopics} name="topics">
1832
+ {['design', 'research', 'testing'].map((topic) => (
1833
+ <InteractionTag key={topic} value={topic}>
1834
+ <InteractionTag.Primary>{topic}</InteractionTag.Primary>
1835
+ </InteractionTag>
1836
+ ))}
1837
+ </TagGroup>
1838
+ );
1839
+ }
1840
+ ```
1841
+
1842
+ - **InteractionTag** is a tag with an action: `InteractionTag.Primary` is a button (it opens a Popover, a Menu, a page), and an optional `InteractionTag.Secondary` is a separate Dismiss button named "Dismiss <text>" (`dismissLabel`). Both are Tab stops; in a TagGroup both join its arrow order, and Delete or Backspace on either dismisses the tag. Fluent's `hasSecondaryAction` is not needed: the Secondary's presence decides.
1843
+ - **The look.** `appearance` (`filled`, `outline`, `brand`), `size` (`medium` 28px, `small` 24px, `extra-small` 20px) and `shape` (`circular`, the default, or `rounded`); `media` (an Avatar, which takes the tag's size), `icon` (decorative) and `secondaryText` (a second line) are slots; `disabled` dims the tag and disables its Dismiss button. Fluent's medium tag is 32px and its default shape `rounded`.
1844
+ - **A link chip.** A Dismiss button cannot sit inside a link or a button, so a dismissible Tag rendered as one warns (`Tag:nested-button`). Put the link in the content instead, `<Tag dismissible><a href="/topics/react">React</a></Tag>`: Delete or Backspace on the link dismisses the tag.
1845
+ - **On its own.** A standalone Tag cannot keep focus once you remove it. When its `onDismiss` removes it, first move focus to a neighbour (the next tag's Dismiss button, else the previous one, else a nearby control), then remove the tag. Prefer a TagGroup, which does this for you.
1846
+
1403
1847
  ### Sorting and selecting in a DataGrid
1404
1848
 
1405
1849
  Sorting is controlled: the grid reports the requested sort and **you reorder the rows**.
@@ -1497,6 +1941,110 @@ export function FileTree() {
1497
1941
  - **Navigation modes.** With `navigationMode="tree"` (the default), Tab from the focused item enters its actions (one stop; the arrow keys move inside them) and Shift+Tab returns. With `"treegrid"`, the "next" arrow (Right, Left in RTL) moves into the actions of an expanded parent or a leaf (a collapsed parent still expands first), "previous" returns, and Up and Down move between items from an action; the actions then take no Tab stop. This replaces APG's "next on an open item moves to its first child" (Down does that). The roles stay `tree` and `treeitem` in both modes, as in Fluent.
1498
1942
  - **Persona rows.** `Tree.ItemPersonaLayout` (flat `TreeItemPersonaLayout`, Fluent's name) lays out an item's label as a person: `media` (an Avatar of size 32 fits a row), the name as children and a `description` under it; the item is named by the name and described by the description.
1499
1943
 
1944
+ ### Navigation
1945
+
1946
+ `Nav` is a side navigation of items, collapsible categories and sub-items. Every child of its list renders a list item, so put only Nav parts there, or wrap other content in an `<li>` (a development warning names the rest):
1947
+
1948
+ ```tsx
1949
+ import { Button, Nav, Tooltip } from '@mortenbrudvik/waveui';
1950
+
1951
+ export function Sidebar() {
1952
+ return (
1953
+ <Nav aria-label="Main" defaultValue="inbox">
1954
+ <Nav.AppItem href="/">Contoso Mail</Nav.AppItem>
1955
+ <Nav.SectionHeader>Mail</Nav.SectionHeader>
1956
+ <Nav.Item
1957
+ value="inbox"
1958
+ href="/inbox"
1959
+ actions={
1960
+ <Tooltip content="New message" relationship="label">
1961
+ <Button appearance="subtle" size="small" icon={<span aria-hidden>+</span>} />
1962
+ </Tooltip>
1963
+ }
1964
+ >
1965
+ Inbox
1966
+ </Nav.Item>
1967
+ <Nav.Divider />
1968
+ <Nav.Item value="settings" href="/settings">
1969
+ Settings
1970
+ </Nav.Item>
1971
+ </Nav>
1972
+ );
1973
+ }
1974
+ ```
1975
+
1976
+ - **Section headers** (`Nav.SectionHeader`) are headings inside list items, `<h3>` by default (`headingLevel`); pick the level that fits the page's outline. **`Nav.Divider`** is a decorative line, hidden from assistive technology (the section header carries the structure).
1977
+ - **App items.** `Nav.AppItem` is the app's name at the top, with an `icon`: a link with `href`, else a button, never selected. `Nav.AppItemStatic` is the same row without an action.
1978
+ - **Item actions.** `actions` on `Nav.Item` and `Nav.SubItem` (Fluent's SplitNavItem) renders your own controls beside the item's link, never inside it, in a group named by the item: a ToggleButton that pins the page, a MenuButton with more commands, each with its own name (a Tooltip with `relationship="label"` names an icon-only one). Each is a Tab stop after its item, and a click on one never selects the item. They are drawn while the row is hovered or has focus, while one is pressed or has its popup open, and always on touch screens and in forced colours; otherwise they are transparent and still focusable.
1979
+
1980
+ ### Navigation drawers and app shells
1981
+
1982
+ `NavDrawer` puts Nav's items and selection in a drawer panel, and `Hamburger` toggles it. Keep the drawer controlled, and pass the page's Hamburger as `finalFocusRef`, so focus returns to it when a Hamburger inside the drawer closes it:
1983
+
1984
+ ```tsx
1985
+ import * as React from 'react';
1986
+ import { Hamburger, Nav, NavDrawer } from '@mortenbrudvik/waveui';
1987
+
1988
+ export function Shell({ narrow }: { narrow: boolean }) {
1989
+ const [open, setOpen] = React.useState(!narrow);
1990
+ const outer = React.useRef<HTMLButtonElement>(null);
1991
+ return (
1992
+ <div className="flex">
1993
+ <Hamburger
1994
+ ref={outer}
1995
+ aria-expanded={open}
1996
+ aria-controls={open ? 'main-nav' : undefined}
1997
+ aria-haspopup={narrow ? 'dialog' : undefined}
1998
+ onClick={() => setOpen(!open)}
1999
+ />
2000
+ <NavDrawer
2001
+ id="main-nav"
2002
+ aria-label="Main"
2003
+ type={narrow ? 'overlay' : 'inline'}
2004
+ open={open}
2005
+ onOpenChange={setOpen}
2006
+ // Every activation, the current page included: an overlay closes on it.
2007
+ onItemSelect={() => {
2008
+ if (narrow) setOpen(false);
2009
+ }}
2010
+ finalFocusRef={outer}
2011
+ defaultValue="home"
2012
+ >
2013
+ {narrow && (
2014
+ <NavDrawer.Header>
2015
+ <Hamburger />
2016
+ Contoso HR
2017
+ </NavDrawer.Header>
2018
+ )}
2019
+ <NavDrawer.Body>
2020
+ <Nav.Item value="home" href="/">
2021
+ Home
2022
+ </Nav.Item>
2023
+ <Nav.Item value="reports" href="/reports">
2024
+ Reports
2025
+ </Nav.Item>
2026
+ </NavDrawer.Body>
2027
+ <NavDrawer.Footer>
2028
+ <Nav.Item value="settings" href="/settings">
2029
+ Settings
2030
+ </Nav.Item>
2031
+ </NavDrawer.Footer>
2032
+ </NavDrawer>
2033
+ <main className="min-w-0 flex-1">…</main>
2034
+ </div>
2035
+ );
2036
+ }
2037
+ ```
2038
+
2039
+ - **Composition.** `NavDrawer.Header` (a row: a Hamburger, the app's name), `NavDrawer.Body` (the scrolling list) and `NavDrawer.Footer` (a list pinned below it), in the order written. Nav parts go in the Body and the Footer; a list item elsewhere warns (`NavDrawer:loose-items`).
2040
+ - **`type` and the breakpoint.** `overlay` (the default) is a modal dialog around a `<nav>` (or `modalType="non-modal"`, which keeps the page usable: keep the page's controls clear of it), rendered in a portal in the browser only; `inline` is part of the page layout, also in the server HTML. A responsive shell switches `type` at a breakpoint and sets the open state when the breakpoint changes (open inline on wide screens, closed as an overlay on narrow ones), so a resize never turns an open pane into a page-blocking modal. Changing `type` remounts the panel and keeps the selection and the open categories. The NavDrawer stories' App shell composes a Toolbar with a Hamburger, a NavDrawer and an inline details Drawer this way.
2041
+ - **The Hamburger** is a `subtle` icon-only Button named "Navigation" (`aria-label` localizes it; its name never changes, the state is `aria-expanded`). Inside a NavDrawer it toggles the drawer and renders `aria-expanded` and `aria-controls`; outside, wire it yourself, as above.
2042
+ - **Closing on selection.** `onItemSelect(value)` fires on every activation, also when the current page is activated again (not for a disabled item or a link opened in another tab): close an overlay there. `onValueChange` fires only when the value changes, so it misses a tap on the current page. Fluent's `onNavItemSelect`.
2043
+ - **The landmark.** One `<nav>`, named "Navigation" by default; an overlay is also a dialog with that name. Rename it (`aria-label`, `aria-labelledby`) when the page has another navigation landmark.
2044
+ - **`focusMode`.** Every item is a Tab stop by default (`NavDrawer.Body focusMode="tab"`). `focusMode="arrow"` makes the body one Tab stop, at first the current page, with both arrow pairs (wrapping), Home and End; screen readers do not announce arrow keys in a navigation landmark, so prefer the default unless your users expect an app pane. Fluent's NavDrawer uses arrow keys by default (its `tabbable` adds Tab stops).
2045
+ - **Width.** `size` defaults to `small` (320px), Drawer's scale; Fluent's navigation pane is 260px wide, `className="w-65"` on your Tailwind build (or a class of your own with `width: 260px`). `position` defaults to `start` (Drawer's to `end`), and `separator` to on.
2046
+ - **Server rendering.** An inline NavDrawer renders in the server HTML (open, or hidden when kept mounted); with `focusMode="arrow"` its Tab stop is set once it has mounted.
2047
+
1500
2048
  ### Slots
1501
2049
 
1502
2050
  Props such as `icon`, `contentBefore` and `dismiss` are slots. A slot accepts content (rendered inside the slot's element) or an object with `as`, `className`, `children` and any attribute of the slot's element:
@@ -1515,107 +2063,165 @@ export const Price = () => (
1515
2063
 
1516
2064
  A slot takes any `React.ReactNode`. Spread an attributes object typed by an interface into a new object (`image={{ ...imgProps }}` for a `React.ImgHTMLAttributes<HTMLImageElement>`).
1517
2065
 
1518
- Icon slots are decorative (`aria-hidden`). Content that renders nothing (`''`, `[]`, `<></>`, or a collection of only `null`, booleans and `''`) counts as no content: a `contentAfter` that renders nothing takes no room, and SearchBox's `contentBefore` keeps the default search icon. The dismiss and clear slots of `MessageBar`, `SearchBox` and `Tag` render their content inside the component's own button; the 0.4 form that passed a button object is deprecated. A Wave `Button` passed there is merged into that button, and its own props never reach the DOM: its `icon` and `iconPosition` place the icon, and `disabledFocusable` makes the built-in button unavailable but focusable. Content that renders nothing keeps the default icon, and a slot object without children (`{ className: 'text-error' }`) wraps it. A slot object that renders a component or a void element (`{ as: CloseIcon }`, `{ as: 'img', src, alt: '' }`) or sets `dangerouslySetInnerHTML` is the icon itself. On `MessageBar`, `dismiss={null}` is the only value that hides the dismiss button (a boolean counts as no slot).
2066
+ Icon slots are decorative (`aria-hidden`). Content that renders nothing (`''`, `[]`, `<></>`, or a collection of only `null`, booleans and `''`) counts as no content: a `contentAfter` that renders nothing takes no room, and SearchBox's `contentBefore` keeps the default search icon. The dismiss and clear slots of `MessageBar`, `SearchBox` and `Tag`, and the `closeButton` of `Dialog.Content` and Drawer, render their content inside the component's own button; the 0.4 form that passed a button object is deprecated. A Wave `Button` passed there is merged into that button, and its own props never reach the DOM: its `icon` and `iconPosition` place the icon, and `disabledFocusable` makes the built-in button unavailable but focusable. Content that renders nothing keeps the default icon, and a slot object without children (`{ className: 'text-error' }`) wraps it. A slot object that renders a component or a void element (`{ as: CloseIcon }`, `{ as: 'img', src, alt: '' }`) or sets `dangerouslySetInnerHTML` is the icon itself. On `MessageBar`, `dismiss={null}` is the only value that hides the dismiss button (a boolean counts as no slot), and so is `closeButton={null}` for the Close button of a Dialog or Drawer.
2067
+
2068
+ The `backdrop` slot of `Dialog.Content` and Drawer takes content or a slot object with `appearance` (`'dimmed'`, `'transparent'`); `null` renders none, and booleans are no slot. Tag's `media` and `icon` (and an InteractionTag Primary's) are content slots: `media` is not hidden from assistive technology (pass `decorative` to an Avatar whose name repeats the tag's text), `icon` is decorative.
1519
2069
 
1520
2070
  Slots that replace a built-in glyph follow one rule per kind:
1521
2071
 
1522
- - **Optional indicators** (`MenuButton.menuIcon`, the `expandIcon` of Combobox, TimePicker and `Tree.Item`): `null` or `undefined` keep the default glyph; `false` or any value that renders nothing hides it (for Combobox and TimePicker, the whole expand button).
1523
- - **Required indicators** (`SplitButton.menuIcon`): the indicator always shows. A value that renders nothing keeps the default glyph, with a development warning.
2072
+ - **Optional indicators** (`MenuButton.menuIcon`, the `expandIcon` of Combobox, Dropdown, TimePicker, TagPicker (and `TagPicker.Control`) and `Tree.Item`, `EmptySwatch.icon`): `null` or `undefined` keep the default glyph; `false` or any value that renders nothing hides it (for Combobox, TimePicker and TagPicker, the whole expand button; for Dropdown, the chevron and its room).
2073
+ - **Required indicators** (`SplitButton.menuIcon`, `Option.checkIcon`, `Hamburger.icon`): the indicator always shows. A value that renders nothing keeps the default glyph, with a development warning. The check is the sign of an option's selection that does not rely on color, so Fluent's `checkIcon={null}`, which removes it, keeps it here.
1524
2074
  - **Status icons** (`MessageBar.icon`, `Toast.icon`, Field `validationMessageIcon`): only `undefined` keeps the default icon; `null` or a value that renders nothing shows none.
1525
- - A glyph slot inside a built-in button (`expandIcon`, and the `menuIcon` of SplitButton and MenuButton) is decorative content. A `<button>` or `Button` passed there, or a slot object whose `as` is one, is not nested: its children become the glyph and its props are dropped, with a development warning.
2075
+ - A glyph slot inside a built-in button (`expandIcon`, the `menuIcon` of SplitButton and MenuButton, the `icon` of `ColorSwatch`, `EmptySwatch` and `Hamburger`, and the children of `InteractionTag.Secondary`, which is itself the button) is decorative content. A `<button>` or `Button` passed there, or a slot object whose `as` is one, is not nested: its children become the glyph and its props are dropped, with a development warning.
1526
2076
 
1527
2077
  ### Built-in text
1528
2078
 
1529
2079
  The names and hidden texts that components render themselves are English by default. These props translate them:
1530
2080
 
1531
- | Component | Props |
1532
- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1533
- | `Dialog.Content`, `Drawer` | `closeLabel` (the Close button) |
1534
- | `TeachingPopover` | `closeLabel` (the Close button), `labels` (`back`, `next`, `done`, `step(index, count)` with a zero-based index) |
1535
- | `MessageBar`, `Toast` | `statusLabel` (the hidden severity text; also an option of both `dispatchToast` forms); `Toast` also `dismissLabel` (also an option of the text form) |
1536
- | `Tag` | `dismissLabel` |
1537
- | `Carousel` | `labels` (`previous`, `next`, `picker`, `slide(index, total)` with a zero-based index, `carouselRoleDescription`, `slideRoleDescription`), `autoPlayLabels` |
1538
- | `Stepper` | `statusLabels` (`completed`, `error`; `''` omits the text) |
1539
- | `Pagination` | `getItemAriaLabel(type, page, selected)` |
1540
- | `ColorPicker` | `labels` (the 0.7 strings, plus `area` (the name of a bound area's group, "Saturation and brightness"), `saturation`, `brightness`, `roleDescription`, `hue`, `transparency`, `colorName(color)` and `valueText(color, channel)` for bound parts) |
1541
- | `ColorArea` | `labels` (`saturation`, `brightness`, `roleDescription` ("2D slider"), `colorName(color)`, `valueText(color, channel)`) |
1542
- | `ColorSlider` | `labels` (`hue`, `saturation`, `brightness` (the `value` channel), `colorName(color)`, `valueText(color, channel)`) |
1543
- | `AlphaSlider` | `labels` (`opacity`, `transparency`, `valueText(color, transparency)`) |
1544
- | `Combobox` | `labels` (`noMatches`, `clear` (the clear button), `expand` (the chevron button)) |
1545
- | `Dropdown` | `labels` (`clear`, the clear button) |
1546
- | `DatePicker` | `labels` (`clear`, `openCalendar`, `previousMonth`, `nextMonth`, and the error texts `invalidDate(pattern)`, `outOfRange(min, max)`, `unavailableDate`) |
1547
- | `TimePicker` | `labels` (`clear`, `expand`, `list`, `noTimes`, `noMatches`, and the error texts `invalidTime(format)`, `outOfRange(min, max)`) |
1548
- | `TagPicker` | `labels` (`remove(label)`, `selected`, `summary(labels)`, `added(label, count)`, `removed(label, count)`, `noMatches`) |
1549
- | `Rating` | `labels` (`star(value, max)`; it also names the half values of a `step={0.5}` Rating, "2.5 stars") |
1550
- | `RatingDisplay` | `labels` (`rating(value, max, formattedValue)`, `count(count, formattedCount)`); `locale` formats the value and the count (pass it when rendering on the server) |
1551
- | `SpinButton` | `labels` (`increment`, `decrement`) |
1552
- | `DataGrid` | `labels` (`selectAll`, `selectionHeader`); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
1553
- | `AvatarGroup` | `overflowLabel`, `unnamedMemberLabel` |
1554
- | `InfoButton` | `aria-label` (the button and its note; default "Information") |
1555
- | `InfoLabel`, `SplitButton` | `infoButtonLabel` (InfoLabel's button is named "‹label› ‹infoButtonLabel›"), `menuButtonLabel` |
1556
- | `Spinner`, `Skeleton.Group` | `label` |
2081
+ | Component | Props |
2082
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2083
+ | `Dialog.Content`, `Drawer` | `closeLabel` (the Close button; on Drawer also the Close button of each `Drawer.HeaderTitle`; a text label of a button merged into `closeButton` names it instead) |
2084
+ | `TeachingPopover` | `closeLabel` (the Close button), `labels` (`back`, `next`, `done`, `step(index, count)` with a zero-based index) |
2085
+ | `MessageBar`, `Toast` | `statusLabel` (the hidden severity text; also an option of both `dispatchToast` forms); `Toast` also `dismissLabel` (also an option of the text form) |
2086
+ | `Tag` | `dismissLabel` ("Dismiss", with the tag's text) |
2087
+ | `InteractionTag.Secondary` | `dismissLabel` ("Dismiss", with the primary text: "Dismiss Cherry") |
2088
+ | `Carousel` | `labels` (`previous`, `next`, `picker`, `slide(index, total)` with a zero-based index, `carouselRoleDescription`, `slideRoleDescription`), `autoPlayLabels` |
2089
+ | `Stepper` | `statusLabels` (`completed`, `error`; `''` omits the text) |
2090
+ | `Pagination` | `getItemAriaLabel(type, page, selected)` |
2091
+ | `ColorPicker` | `labels` (the 0.7 strings, plus `area` (the name of a bound area's group, "Saturation and brightness"), `saturation`, `brightness`, `roleDescription`, `hue`, `transparency`, `colorName(color)` and `valueText(color, channel)` for bound parts) |
2092
+ | `ColorArea` | `labels` (`saturation`, `brightness`, `roleDescription` ("2D slider"), `colorName(color)`, `valueText(color, channel)`) |
2093
+ | `ColorSlider` | `labels` (`hue`, `saturation`, `brightness` (the `value` channel), `colorName(color)`, `valueText(color, channel)`) |
2094
+ | `AlphaSlider` | `labels` (`opacity`, `transparency`, `valueText(color, transparency)`) |
2095
+ | `Combobox` | `labels` (`noMatches`, `clear` (the clear button), `expand` (the chevron button), and with `multiselect` `selection(labels)` (the selected labels the input shows, joined with `", "`), `added(label, count)` and `removed(label, count)` (the toggle announcements, "Olives added, 2 selected", "Olives removed, 1 selected")) |
2096
+ | `Dropdown` | `labels` (`clear` (the clear button), and with `multiselect` `selection(labels)` (the button's text), `added(label, count)` and `removed(label, count)`, as in Combobox) |
2097
+ | `Listbox` | `labels` (`added(label, count)` and `removed(label, count)`, the `multiselect` toggle announcements, as in Combobox) |
2098
+ | `DatePicker` | `labels` (`clear`, `openCalendar`, `close` (the Close button, `showCloseButton`), the calendar's labels below (not `required`), and the error texts `invalidDate(pattern)`, `outOfRange(min, max)`, `unavailableDate`) |
2099
+ | `Calendar` | `labels` (the header buttons `previousMonth`, `nextMonth`, `previousYear`, `nextYear`, `previousDecade`, `nextDecade`; the heading button's hints `chooseMonth` ("Choose a month") and `chooseYear` ("Choose a year"); `goToToday`; the week-number column `week` ("Week") and `weekNumber(week)` ("Week 23"); `markedDay(dayLabel)` ("Monday, June 2, 2025, marked"); `required` ("Required", the hint of a required calendar)) |
2100
+ | `SwatchPicker` | `labels` (`required`: "Required", said in the description where the picker's role cannot carry `aria-required`, the grid layout and tab mode) |
2101
+ | `EmptySwatch` | `aria-label` ("Add color") |
2102
+ | `TimePicker` | `labels` (`clear`, `expand`, `list`, `noTimes`, `noMatches`, and the error texts `invalidTime(format)`, `outOfRange(min, max)`) |
2103
+ | `TagPicker` | `labels` (`remove(label)`, `selected`, `summary(labels)`, `added(label, count)`, `removed(label, count)`, `noMatches`, `create(text)` (`Create "{text}"`), `clear` ("Clear selection"), `expand` ("Show options"), `overflowCount(count)` ("+{count}")) |
2104
+ | `Rating` | `labels` (`star(value, max)`; it also names the half values of a `step={0.5}` Rating, "2.5 stars") |
2105
+ | `RatingDisplay` | `labels` (`rating(value, max, formattedValue)`, `count(count, formattedCount)`); `locale` formats the value and the count (pass it when rendering on the server) |
2106
+ | `SpinButton` | `labels` (`increment`, `decrement`) |
2107
+ | `DataGrid` | `labels` (`selectAll`, `selectionHeader`); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
2108
+ | `AvatarGroup` | `overflowLabel`, `unnamedMemberLabel` |
2109
+ | `InfoButton` | `aria-label` (the button and its note; default "Information") |
2110
+ | `Hamburger` | `aria-label` ("Navigation", only while it has no other name) |
2111
+ | `NavDrawer` | `aria-label` ("Navigation", the landmark and an overlay's dialog) |
2112
+ | `InfoLabel`, `SplitButton` | `infoButtonLabel` (InfoLabel's button is named "‹label› ‹infoButtonLabel›"), `menuButtonLabel` |
2113
+ | `Spinner`, `Skeleton.Group` | `label` |
1557
2114
 
1558
2115
  Without a text prop, these defaults are replaced through `aria-label`: the PresenceBadge status names ("Available", …: `aria-label` on the badge), the MessageBar "Dismiss" and SearchBox "Clear search" button names (`dismiss={{ 'aria-label': 'Lukk' }}`, which keeps the default icon), and the names of the Breadcrumb, Pagination, Nav, Stepper, Carousel and Toaster regions ("Breadcrumb", "Pagination", "Navigation", "Progress", "Carousel", "Notifications"). The placeholders of Dropdown, DatePicker, TimePicker and SearchBox are `placeholder` props, and ColorPicker's default presets are replaced through `presets`.
1559
2116
 
1560
- 0.7 and 0.8 add no other built-in text: the submenu half of a `Menu.SplitGroup`, a context menu (`Menu.Popover`) and a `Toolbar.RadioGroup` are named by the `aria-label` you pass, a toast's container by its severity and title, and a tree row's actions toolbar by its item's label.
2117
+ 0.7 and 0.8 add no other built-in text: the submenu half of a `Menu.SplitGroup`, a context menu (`Menu.Popover`) and a `Toolbar.RadioGroup` are named by the `aria-label` you pass, a toast's container by its severity and title, and a tree row's actions toolbar by its item's label. Nor do 0.10 and 0.11, beyond the rows above: a `TagGroup` is named by the `aria-label` you pass, an inline Drawer by its title; a `Listbox`, a `Calendar` and a `ColorSwatch` or `ImageSwatch` are named by the `aria-label` you pass (or a `Field`'s label, or a `Tooltip` around the swatch), an `OptionGroup` by its label, and the month and year buttons by their text in `locale`.
1561
2118
 
1562
2119
  ## Keyboard support
1563
2120
 
1564
- As implemented in 0.9. Buttons, links, checkboxes, switches and the trigger buttons of Accordion, Carousel, Nav, Breadcrumb and Pagination are native elements: Tab to reach them, Enter or Space to activate. A Link or `Button as="a"` without `href`, and a Button or Link rendered as a `div` or `span`, are buttons: Tab reaches them, Enter (key down) and Space (key up) activate them. A `disabledFocusable` control stays in the Tab order but Enter, Space and clicks do nothing.
1565
-
1566
- | Component | Keys |
1567
- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1568
- | Toolbar | One Tab stop (the last focused control; focusing a nested composite, such as a radio group or tab list, or a text field, select, slider or spin button keeps it where it was). Left/Right (Up/Down when vertical) move and wrap, Home/End jump; mirrored in RTL; natively disabled controls and controls hidden with CSS are skipped, while `disabledFocusable` controls stay reachable (a Tooltip on them opens on focus). Text fields, selects, sliders, spin buttons and editable comboboxes keep their own arrow keys; a Dropdown keeps Up/Down, Home and End for its list, but Left/Right move past it. A nested composite keeps its own Tab stop and arrow keys; a `Toolbar.RadioGroup` is none: its radios are part of the arrow order, and Up/Down (Left/Right when vertical, mirrored in RTL) also move among them, wrapping inside the group. The arrows never press a toggle or check a radio: Space and Enter do. |
1569
- | RadioGroup, SwatchPicker | One Tab stop. All four arrows move and select (wrapping), Home/End jump; Left/Right mirrored in RTL. Keys pressed on a link inside a radio's label stay the link's. |
1570
- | Rating | Right/Up one step more (a star, or half a star with `step={0.5}`), Left/Down one fewer (never below `step`), Home/End `step`/`max`; mirrored in RTL. A pointer click on a half-star Rating chooses the half or the whole star by position; Space and Enter on a focused radio choose that radio's own value. |
1571
- | TabList | One Tab stop (the selected tab). Left/Right (Up/Down when vertical) move and select, Home/End; with `selectTabOnFocus={false}` they only move focus, and Enter or Space selects the focused tab; disabled tabs skipped; mirrored in RTL. |
1572
- | Tree | One Tab stop. Up/Down, Home/End; Right expands or moves to the first child, Left collapses or moves to the parent (mirrored in RTL); Enter activates; Space activates, or in a checkbox tree (`selectionMode="multiple"`) toggles the focused item's check; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). Row actions: Tab from the focused item enters them and Shift+Tab returns (`navigationMode="tree"`); with `navigationMode="treegrid"` the "next" arrow moves into them (on an expanded parent too, instead of to its first child), "previous" returns, and Up/Down move between items. |
1573
- | List (selectable) | One Tab stop. Up/Down (wrapping), Home/End, Enter/Space toggle, typeahead above 7 items (a Space within 500 ms of a letter continues the search). With item actions (grid): Up/Down between rows, Left/Right into the actions, Enter/F2 into a text field, Escape back. |
1574
- | DataGrid | One Tab stop. Arrows between cells (Left/Right to the previous/next cell of the row, mirrored in RTL; Up/Down and PageUp/PageDown keep the visual column across `colSpan`/`rowSpan` cells of grouped headers), Home/End row start/end, Ctrl+Home/Ctrl+End grid start/end, PageUp/PageDown 10 rows; Enter/F2 into a cell's widgets, Escape back; Space on a cell toggles its row's selection. |
1575
- | Menu | Trigger: Enter/Space/Down open and focus the first item, Up opens on the last; a trigger with `aria-disabled="true"` (a `disabledFocusable` MenuButton or SplitButton) ignores them and its click. Menu: Up/Down (wrapping), Home/End, typeahead, Enter/Space activate (a Space within 500 ms of a letter continues the search); on a checkbox, radio or switch item Space changes it and keeps the menu open, Enter changes it and closes a popup menu; on `Menu.ItemLink` Enter is the browser's link activation. A submenu's item: Right (Left in RTL), Enter and Space open the submenu and focus its first item; in a submenu Left (Right in RTL) and Escape close only it and return focus to its item. `Menu.SplitGroup`: Right (Left in RTL) moves from the action to the submenu button, Left back. Escape closes and returns focus, Tab closes every level. Context menu (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it at the focused element; Escape and item activation return focus there, and Tab moves on from it. |
1576
- | Combobox, TagPicker, TimePicker | Down/Up open and move, Alt+Down opens, typing makes the first match active (not in a `freeform` Combobox), Enter commits the active option (with no active option, Enter submits the form; TimePicker instead never submits while its text is edited: a complete time within the bounds is committed, erased text clears the value, other text is kept and flagged invalid, and the list closes; leaving the input with edited text settles it the same way and closes the list), Alt+Up and Tab close, Escape closes and then discards typed text. The chevron button (Combobox, TimePicker) is not a Tab stop; the clear button (`clearable`) is a Tab stop after the input. Tab from erased TimePicker text clears the value first, so focus moves past the clear button that goes with it. TagPicker: Backspace in the empty input focuses the last tag, Backspace/Delete removes it. |
1577
- | Dropdown | Down/Up/Home/End and typing open and move, PageUp/PageDown move by 10, Enter/Space commit (a Space within 500 ms of a letter continues the search), Alt+Up and Tab commit and close, Escape closes. The clear button (`clearable`) is a Tab stop after the combobox button. |
1578
- | SpinButton | Up/Down step, PageUp/PageDown large step, Home/End jump to a finite min/max (otherwise they move the caret; with Shift they select text); Enter commits typed text, Escape reverts it. With the pointer, holding a step button repeats the step; a short press steps once on release. |
1579
- | SearchBox | Escape clears the text and keeps focus (the key is consumed, so an enclosing Dialog, Drawer or Popover stays open); in an empty field Escape reaches the enclosing overlay. |
1580
- | DatePicker | Alt+Down opens the calendar. The clear button (`clearable`) is a Tab stop between the input and the calendar button; Tab from erased text clears the value first and moves on to the calendar button. Calendar: arrows by day/week (mirrored in RTL), PageUp/PageDown by month, Shift+PageUp/PageDown by year, Home/End week start/end, Enter/Space select, Escape closes; Tab stays inside. |
1581
- | Dialog, Drawer | Focus is trapped; Escape closes; focus returns to the first of these that can take focus: `finalFocusRef`, the element that had focus when the modal opened (a trigger's wrapper span resolves to the element inside it), the trigger, an element next to where that opener was. |
1582
- | Popover, TeachingPopover | Escape closes. When focus was inside (or lost to `<body>`), Popover returns it to its trigger, TeachingPopover to where focus was before it opened; a close by hover moves no focus. In the Tab order the content follows its trigger (TeachingPopover with `target`: the target): Tab from the trigger enters it, Tab past its end continues after the trigger, Shift+Tab from the element after the trigger enters it at its last element. For a trigger outside the tab order it follows the tab stop before the trigger; with no tab stop before the trigger or target, it is reached at the end of the page. A Tab lap visits it once: Tab from the last element of the page leaves the page, and Shift+Tab from outside the page reaches the page's last element. Focus leaving it does not close it. A context popover (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it and moves focus into the content, and Escape returns focus to the element that had it; opened by a right click, it is reached by Tab from that element. |
1583
- | Tooltip, InfoButton, InfoLabel | Open on keyboard focus (Tooltip after `openDelay`); Escape closes. InfoButton and InfoLabel: a click pins the note; focus stays on the button when the note opens, Tab from the button enters the note's first focusable element (a link) and Tab past its last element continues after the button; Shift+Tab returns; Escape closes it and returns focus to the button when it was in the note. `openOnHover={false}` opens it on a click (Enter, Space) only. |
1584
- | Card (selectable) | With the default `selectionControl="card"`: Enter (key down) or Space (key up) selects. With `"checkbox"`: the built-in checkbox. |
1585
- | Stepper | Each reachable step is a Tab stop; Enter/Space activate. |
1586
- | Slider | Native range keys. |
1587
- | Toaster | The app's `shortcuts.focus` key press focuses the newest toast; Tab and Shift+Tab move through the toasts' controls; Escape dismisses every toast and returns focus (a tooltip or menu of a toast closes first); Delete or Backspace dismisses the focused toast (the keys are ignored in editable fields), and focus moves to the next toast, or the previous one, else back to where it was before the toasts. |
1588
- | ColorArea | One Tab stop. Left/Right change saturation (mirrored in RTL), Up/Down brightness; Shift with an arrow by 10; PageUp/PageDown brightness by 10; Home/End saturation to 0/100. |
1589
- | ColorSlider, AlphaSlider | The arrows of their axis by 1 (mirrored in RTL when horizontal), Shift with an arrow by 10, PageUp/PageDown by 10, Home/End to the minimum/maximum. |
2121
+ As implemented in 0.11. Buttons, links, checkboxes, switches and the trigger buttons of Accordion, Carousel, Nav, Breadcrumb and Pagination are native elements: Tab to reach them, Enter or Space to activate. A Link or `Button as="a"` without `href`, and a Button or Link rendered as a `div` or `span`, are buttons: Tab reaches them, Enter (key down) and Space (key up) activate them. A `disabledFocusable` control stays in the Tab order but Enter, Space and clicks do nothing.
2122
+
2123
+ | Component | Keys |
2124
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2125
+ | Toolbar | One Tab stop (the last focused control; focusing a nested composite, such as a radio group or tab list, or a text field, select, slider or spin button keeps it where it was). Left/Right (Up/Down when vertical) move and wrap, Home/End jump; mirrored in RTL; natively disabled controls and controls hidden with CSS are skipped, while `disabledFocusable` controls stay reachable (a Tooltip on them opens on focus). Text fields, selects, sliders, spin buttons and editable comboboxes keep their own arrow keys; a Dropdown keeps Up/Down, Home and End for its list, but Left/Right move past it. A nested composite keeps its own Tab stop and arrow keys; a `Toolbar.RadioGroup` is none: its radios are part of the arrow order, and Up/Down (Left/Right when vertical, mirrored in RTL) also move among them, wrapping inside the group. The arrows never press a toggle or check a radio: Space and Enter do. |
2126
+ | TagGroup | Toolbar (the default): one Tab stop, the last focused control. Left/Right move between the controls (Up/Down with `orientation="vertical"`), mirrored in RTL, wrapping in DOM order across wrapped rows; Home/End jump; natively disabled controls are skipped. Delete or Backspace on a Dismiss button, on either button of an InteractionTag with a Secondary, or on a link in a dismissible tag's text dismisses the tag (a held key dismisses one tag). When the focused tag is removed, focus moves to the same control of the next tag, else the previous one, else `fallbackFocusRef`. In a selecting group Space or Enter on an `InteractionTag.Primary` toggles it. `role="list"`: every control is a Tab stop, and the group handles no arrow key. |
2127
+ | InteractionTag | Two Tab stops, the Primary and the Secondary; in a TagGroup both join its arrow order. Enter or Space activates the Primary; Delete or Backspace on either button dismisses the tag (with a Secondary), as Enter, Space and a click on the Secondary do. |
2128
+ | Tag | The Dismiss button: Enter, Space, Delete and Backspace dismiss (a held key dismisses once), also outside a TagGroup. Delete or Backspace on a link in a dismissible tag's text (`<Tag dismissible><a href>…</a></Tag>`) clicks its Dismiss button. |
2129
+ | RadioGroup, SwatchPicker (row layout) | One Tab stop. All four arrows move and select (wrapping), Home/End jump; Left/Right mirrored in RTL. Keys pressed on a link inside a radio's label stay the link's. An `EmptySwatch` in the row layout is a Tab stop of its own, and the arrow keys pressed on it do nothing. |
2130
+ | SwatchPicker (`layout="grid"`) | One Tab stop: the last focused swatch, at first the selected one, else the first. The arrows move focus only: Left/Right through the swatches in reading order across the rows (mirrored in RTL), stopping at the grid's first and last swatch; Up/Down to the enabled swatch of the previous/next row nearest the same column, stopping at the first and last row; Home/End to the first/last swatch of the row, Ctrl+Home/Ctrl+End of the grid. Space, Enter and a click select. Disabled swatches are skipped; an `EmptySwatch` is a cell like the others. |
2131
+ | SwatchPicker (`focusMode="tab"`) | Every enabled swatch is a Tab stop, and the arrow keys do nothing; Space, Enter and a click select (the selected swatch stays selected). |
2132
+ | Listbox | One Tab stop: the list. The active option shows only while the list has focus (the selected option, else the first). Down/Up move without wrapping, Home/End, PageUp/PageDown by 10, typing moves to the matching option (a Space within 500 ms of a letter continues the search); Space and Enter select, or with `multiselect` toggle. Tab, Escape and Alt with an arrow are left to the page (an enclosing Dialog closes on Escape). With `disabledOptionsFocusable` the keys reach disabled options, but Space and Enter do nothing on them. |
2133
+ | Rating | Right/Up one step more (a star, or half a star with `step={0.5}`), Left/Down one fewer (never below `step`), Home/End `step`/`max`; mirrored in RTL. A pointer click on a half-star Rating chooses the half or the whole star by position; Space and Enter on a focused radio choose that radio's own value. |
2134
+ | TabList | One Tab stop (the selected tab). Left/Right (Up/Down when vertical) move and select, Home/End; with `selectTabOnFocus={false}` they only move focus, and Enter or Space selects the focused tab; disabled tabs skipped; mirrored in RTL. |
2135
+ | Tree | One Tab stop. Up/Down, Home/End; Right expands or moves to the first child, Left collapses or moves to the parent (mirrored in RTL); Enter activates; Space activates, or in a checkbox tree (`selectionMode="multiple"`) toggles the focused item's check; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). Row actions: Tab from the focused item enters them and Shift+Tab returns (`navigationMode="tree"`); with `navigationMode="treegrid"` the "next" arrow moves into them (on an expanded parent too, instead of to its first child), "previous" returns, and Up/Down move between items. |
2136
+ | List (selectable) | One Tab stop. Up/Down (wrapping), Home/End, Enter/Space toggle, typeahead above 7 items (a Space within 500 ms of a letter continues the search). With item actions (grid): Up/Down between rows, Left/Right into the actions, Enter/F2 into a text field, Escape back. |
2137
+ | DataGrid | One Tab stop. Arrows between cells (Left/Right to the previous/next cell of the row, mirrored in RTL; Up/Down and PageUp/PageDown keep the visual column across `colSpan`/`rowSpan` cells of grouped headers), Home/End row start/end, Ctrl+Home/Ctrl+End grid start/end, PageUp/PageDown 10 rows; Enter/F2 into a cell's widgets, Escape back; Space on a cell toggles its row's selection. |
2138
+ | Menu | Trigger: Enter/Space/Down open and focus the first item, Up opens on the last; a trigger with `aria-disabled="true"` (a `disabledFocusable` MenuButton or SplitButton) ignores them and its click. Menu: Up/Down (wrapping), Home/End, typeahead, Enter/Space activate (a Space within 500 ms of a letter continues the search); on a checkbox, radio or switch item Space changes it and keeps the menu open, Enter changes it and closes a popup menu; on `Menu.ItemLink` Enter is the browser's link activation. A submenu's item: Right (Left in RTL), Enter and Space open the submenu and focus its first item; in a submenu Left (Right in RTL) and Escape close only it and return focus to its item. `Menu.SplitGroup`: Right (Left in RTL) moves from the action to the submenu button, Left back. Escape closes and returns focus, Tab closes every level. Context menu (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it at the focused element; Escape and item activation return focus there, and Tab moves on from it. |
2139
+ | Combobox, TagPicker, TimePicker | Down/Up open and move, Alt+Down opens, typing makes the first match active (not in a `freeform` Combobox), Enter commits the active option (with no active option, Enter submits the form; TimePicker instead never submits while its text is edited: a complete time within the bounds is committed, erased text clears the value, other text is kept and flagged invalid, and the list closes; leaving the input with edited text settles it the same way and closes the list), Alt+Up and Tab close, Escape closes and then discards typed text. The chevron button (Combobox, TimePicker) is not a Tab stop; the clear button (`clearable`) is a Tab stop after the input. Tab from erased TimePicker text clears the value first, so focus moves past the clear button that goes with it. TagPicker: Backspace in the empty input, and the previous arrow (Left, Right in RTL) with the caret at the start of the text and nothing selected, focus the last tag; on a tag's remove button Left/Right move between the tags (mirrored in RTL) and past the last one return to the trigger, and Delete, Backspace, Enter or Space remove it and focus the next tag's remove button, else the input (a click focuses the input). Typing makes the option whose label equals the text active, else the first match; with `allowCreate` Enter on the create option adds the text. `TagPicker.Button`: Down, Up and a typed letter open and move, Enter and Space add the active option. The chevron is not a Tab stop; the clear button is, after the trigger. Multi-select (`multiselect`, Combobox): Enter toggles the active option and keeps the list open; Tab, Escape and Alt+Up close without committing; focusing the input selects the shown labels, so typing replaces them with a query, and Backspace and Delete only edit the text. With `disabledOptionsFocusable` (Combobox) the arrow keys reach disabled options, but Enter does nothing on them. |
2140
+ | Dropdown | Down/Up/Home/End and typing open and move, PageUp/PageDown move by 10, Enter/Space commit (a Space within 500 ms of a letter continues the search), Alt+Up and Tab commit and close, Escape closes. The clear button (`clearable`) is a Tab stop after the combobox button. Multi-select (`multiselect`): Enter and Space toggle the active option and keep the list open; Tab, Escape and Alt+Up close without committing. With `disabledOptionsFocusable` the keys reach disabled options, but Enter and Space do nothing on them, and Tab or Alt+Up on one closes the list without selecting. |
2141
+ | SpinButton | Up/Down step, PageUp/PageDown large step, Home/End jump to a finite min/max (otherwise they move the caret; with Shift they select text); Enter commits typed text, Escape reverts it. With the pointer, holding a step button repeats the step; a short press steps once on release. |
2142
+ | SearchBox | Escape clears the text and keeps focus (the key is consumed, so an enclosing Dialog, Drawer or Popover stays open); in an empty field Escape reaches the enclosing overlay. |
2143
+ | DatePicker | Alt+Down opens the calendar. The clear button (`clearable`) is a Tab stop between the input and the calendar button; Tab from erased text clears the value first and moves on to the calendar button. Calendar dialog: the keys of Calendar below; Escape in the month or year view goes back a view, and in the day view closes the calendar and returns focus. Tab stays inside: the previous button, the heading button (`showMonthPicker`), the next button, the grid, then the footer buttons (`showGoToToday`, `showCloseButton`). |
2144
+ | Calendar | One Tab stop in the grid, after the header buttons: the focused day (the selected day of the shown month, else today, else the first day of the month in range). Day view: arrows by day/week (mirrored in RTL), PageUp/PageDown by month, Shift+PageUp/PageDown by year (the day clamped to the month), Home/End week start/end, Enter/Space select. Month view: arrows by a month and a row of four (mirrored in RTL; past December or January they show the next or previous year), Home/End to the row's ends, PageUp/PageDown by a year, Enter/Space show the month's days. Year view: the arrows, Home and End stay on the shown page of 12 years while the target year is on it (the muted years before and after the decade included), PageUp/PageDown move a decade, Enter/Space show the year's months. Escape in the month or year view goes back a view (it is consumed, also from the header and footer buttons); in the day view it is left to the page. |
2145
+ | Dialog, Drawer (modal, alert) | Focus is trapped (in a drawer: header, then body, then footer); Escape closes; the trigger toggles; focus returns to the first of these that can take focus: `finalFocusRef`, the element that had focus when the modal opened (a trigger's wrapper span resolves to the element inside it), the trigger, an element next to where that opener was. |
2146
+ | Dialog, Drawer (non-modal) | Focus moves in on open. Tab and Shift+Tab leave the surface in the page's order right after the element that opened it; Escape closes it only while focus is inside; the trigger toggles; focus returns to the opener (or `finalFocusRef`) only when it was inside. |
2147
+ | Drawer (inline) | Page content: no trap, opening moves no focus, Escape and presses outside never close it; the trigger toggles; closing it while focus is inside returns focus to `finalFocusRef`, else the element that opened it, else the trigger. |
2148
+ | Nav | Every item, category and app item is a Tab stop (Enter or Space activates; a category toggles); an item's `actions` follow it in the Tab order. |
2149
+ | NavDrawer | An overlay as a Drawer (modal or non-modal); an inline one as page content. A Hamburger inside it toggles it, and focus goes to `finalFocusRef` (your page's Hamburger) when it closes with focus inside. |
2150
+ | NavDrawer.Body (`focusMode="arrow"`) | One Tab stop: the last focused element, at first the current page (or the closed category that holds it), else the first item. Down/Right move to the next element and Up/Left to the previous one (Left and Right swap in RTL), wrapping; Home/End jump; item actions join the order. The footer's items stay Tab stops. |
2151
+ | Popover, TeachingPopover | Escape closes. When focus was inside (or lost to `<body>`), Popover returns it to its trigger, TeachingPopover to where focus was before it opened; a close by hover moves no focus. In the Tab order the content follows its trigger (TeachingPopover with `target`: the target): Tab from the trigger enters it, Tab past its end continues after the trigger, Shift+Tab from the element after the trigger enters it at its last element. For a trigger outside the tab order it follows the tab stop before the trigger; with no tab stop before the trigger or target, it is reached at the end of the page. A Tab lap visits it once: Tab from the last element of the page leaves the page, and Shift+Tab from outside the page reaches the page's last element. Focus leaving it does not close it. A context popover (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it and moves focus into the content, and Escape returns focus to the element that had it; opened by a right click, it is reached by Tab from that element. |
2152
+ | Tooltip, InfoButton, InfoLabel | Open on keyboard focus (Tooltip after `openDelay`); Escape closes. InfoButton and InfoLabel: a click pins the note; focus stays on the button when the note opens, Tab from the button enters the note's first focusable element (a link) and Tab past its last element continues after the button; Shift+Tab returns; Escape closes it and returns focus to the button when it was in the note. `openOnHover={false}` opens it on a click (Enter, Space) only. |
2153
+ | Card (selectable) | With the default `selectionControl="card"`: Enter (key down) or Space (key up) selects. With `"checkbox"`: the built-in checkbox. |
2154
+ | Stepper | Each reachable step is a Tab stop; Enter/Space activate. |
2155
+ | Slider | Native range keys. |
2156
+ | Toaster | The app's `shortcuts.focus` key press focuses the newest toast; Tab and Shift+Tab move through the toasts' controls; Escape dismisses every toast and returns focus (a tooltip or menu of a toast closes first); Delete or Backspace dismisses the focused toast (the keys are ignored in editable fields), and focus moves to the next toast, or the previous one, else back to where it was before the toasts. |
2157
+ | ColorArea | One Tab stop. Left/Right change saturation (mirrored in RTL), Up/Down brightness; Shift with an arrow by 10; PageUp/PageDown brightness by 10; Home/End saturation to 0/100. |
2158
+ | ColorSlider, AlphaSlider | The arrows of their axis by 1 (mirrored in RTL when horizontal), Shift with an arrow by 10, PageUp/PageDown by 10, Home/End to the minimum/maximum. |
1590
2159
 
1591
2160
  Accordion and Carousel have no arrow-key navigation: their triggers and controls are Tab stops.
1592
2161
 
1593
- Typeahead (Menu, selectable List, Tree, Dropdown) accepts characters typed with AltGr, which Windows reports as Ctrl+Alt (Polish `ł`, Romanian `ș`); Ctrl+Alt with an arrow, Home, End or Space is left to the browser. An item that cannot take focus is passed over for the next match. Menu typeahead matches the item labels, not icons or shortcuts. Options and option groups with `hidden` are skipped like a native `<option hidden>`.
2162
+ Typeahead (Menu, selectable List, Tree, Dropdown, Listbox) accepts characters typed with AltGr, which Windows reports as Ctrl+Alt (Polish `ł`, Romanian `ș`); Ctrl+Alt with an arrow, Home, End or Space is left to the browser. An item that cannot take focus is passed over for the next match. Menu typeahead matches the item labels, not icons or shortcuts. Options and option groups with `hidden` are skipped like a native `<option hidden>`.
1594
2163
 
1595
2164
  ## Hooks and utilities
1596
2165
 
1597
- | Export | Purpose |
1598
- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1599
- | `useControllable(value, defaultValue, onChange)` | Controlled/uncontrolled state; returns `[value, setValue, isControlled]`; `setValue` accepts a value or an updater and skips no-op updates. |
1600
- | `useRovingTabIndex(options)` | Roving tab stop and arrow-key navigation for composite widgets; items marked `data-disabled-focusable` stay reachable, a composite marked `data-roving-transparent` (a toolbar radio group) adds its items to the enclosing arrow order, and `tabStop: 'none'` leaves every item out of the Tab order (a composite reached by arrow keys only). |
1601
- | `useId(prefix?)` | SSR-safe ids (`prefix-<react id>`; never parse the React part). |
1602
- | `useEventCallback(fn)` | Stable callback that always calls the latest `fn`. |
1603
- | `useMergedRefs(...refs)` | One stable callback ref for several refs. |
1604
- | `useIsClient()` | `false` on the server and during hydration. |
1605
- | `usePresence(visible, options?)`, `<Presence visible>` | Mount and unmount an element with CSS enter and exit motion (`data-presence` phases; see [Enter and exit motion](#enter-and-exit-motion)). |
1606
- | `useFieldControl(props, options?)` | Merge a custom control's labelling props with the surrounding `Field` (its label, validation message, hint and state). |
1607
- | `useAnnounce()`, `announce(message, politeness?)` | Screen-reader announcements through a shared live region. |
1608
- | `useWaveTheme()` | Current theme, direction, portal container and `inputDefaults`. |
1609
- | `useToastController()` | `dispatchToast` (content or options) / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
1610
- | `createToaster()` | A toast controller for code outside components; bind it with `<Toaster toaster={handle}>` (see [Toasts](#toasts)). Server-safe. |
1611
- | `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
1612
- | `hsvToHex(color)`, `hexToHsv(hex)` | Convert between an `HsvColor` and a hex string (`#rrggbb`, or `#rrggbbaa` below full opacity); `hexToHsv` returns `null` for other text. Server-safe. |
1613
- | `buttonClassNames`, `toastTitleClassNames`, … | The stable class names of every component and part (see [Styling hooks](#styling-hooks)). Server-safe. |
1614
- | `cn(...classes)` | clsx + tailwind-merge that knows Wave's type ramp and shadows; the last class wins over a conflicting class of the same variant. |
1615
- | `composeEventHandlers(theirs, ours)` | Run a consumer handler, then yours unless it called `preventDefault()`. |
1616
- | `mergeRefs(...refs)` | Merge refs outside components. |
1617
- | `resolveSlot`, `renderSlot` | The slot helpers the components use. |
1618
- | `getThemeClassName(theme)` | The theme classes of a theme. |
2166
+ | Export | Purpose |
2167
+ | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2168
+ | `useControllable(value, defaultValue, onChange)` | Controlled/uncontrolled state; returns `[value, setValue, isControlled]`; `setValue` accepts a value or an updater and skips no-op updates. |
2169
+ | `useRovingTabIndex(options)` | Roving tab stop and arrow-key navigation for composite widgets; items marked `data-disabled-focusable` stay reachable, a composite marked `data-roving-transparent` (a toolbar radio group) adds its items to the enclosing arrow order, and `tabStop: 'none'` leaves every item out of the Tab order (a composite reached by arrow keys only). |
2170
+ | `useId(prefix?)` | SSR-safe ids (`prefix-<react id>`; never parse the React part). |
2171
+ | `useEventCallback(fn)` | Stable callback that always calls the latest `fn`. |
2172
+ | `useMergedRefs(...refs)` | One stable callback ref for several refs. |
2173
+ | `useIsClient()` | `false` on the server and during hydration. |
2174
+ | `usePresence(visible, options?)`, `<Presence visible>` | Mount and unmount an element with CSS enter and exit motion (`data-presence` phases; see [Enter and exit motion](#enter-and-exit-motion)). |
2175
+ | `useFieldControl(props, options?)` | Merge a custom control's labelling props with the surrounding `Field` (its label, validation message, hint and state). |
2176
+ | `useAnnounce()`, `announce(message, politeness?)` | Screen-reader announcements through a shared live region. |
2177
+ | `useActiveDescendant(options)` | The `aria-activedescendant` model under the pickers, for composites that keep focus on one element: `items`, `getId`, `getElement` (the item's element for scrolling; by default the element with its id), `enabled`, `fallback`, `loop`, `activateFirstOnChange`, `onActiveValueChange`; returns `activeValue`, `activeDescendantId` and stable `setActiveValue`, `highlight` (pointer moves, not scrolled into view), `first`, `last`, `next`, `prev` and `move`. State-based: it reads no DOM during render, so an active fallback item's id is in the server HTML. |
2178
+ | `useListbox(options)`, `useListboxOption(props)` | The listbox under Dropdown, Combobox, TagPicker and Listbox: keys, typeahead, filtering, the active option and option registration, in `'select-only'`, `'editable'` or `'standalone'` mode, with `multiselect` and `disabledOptionsFocusable`; `onOpenChange(open, details)` and `onSelect(value, details)` (see [Listbox and custom pickers](#listbox-and-custom-pickers)). `useListboxOption` builds an option component of your own. |
2179
+ | `useListboxPopup(options)`, `ListboxSurface` | The popup of a custom picker: dismissal (`onDismiss(reason)`, a `DismissReason`: `'escape'`, `'outside-press'` or `'focus-outside'`) and positioning, and the surface that renders the list (`listClassName`, `surfaceClassName`). |
2180
+ | `ListboxProvider` | Provides a listbox's context to its options when you render the list yourself. |
2181
+ | `collectOptionLabels(children)`, `markListboxElement(component, kind)` | Read option labels from children before the options register (server HTML, first render); mark your own option and group components so they are read. |
2182
+ | `useWaveTheme()` | Current theme, direction, portal container and `inputDefaults`. |
2183
+ | `useToastController()` | `dispatchToast` (content or options) / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
2184
+ | `createToaster()` | A toast controller for code outside components; bind it with `<Toaster toaster={handle}>` (see [Toasts](#toasts)). Server-safe. |
2185
+ | `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
2186
+ | `hsvToHex(color)`, `hexToHsv(hex)` | Convert between an `HsvColor` and a hex string (`#rrggbb`, or `#rrggbbaa` below full opacity); `hexToHsv` returns `null` for other text. Server-safe. |
2187
+ | `addDays(date, amount)`, `addMonths(date, amount)`, `isSameDay(a, b)`, `startOfWeek(date, firstDayOfWeek?)`, `getWeekNumber(date, options?)` | Date helpers on calendar days: every returned date is local midnight (Fluent's keep the time of day). `addMonths` clamps the day to the target month (January 31 plus a month is February 28 or 29); `getWeekNumber` takes `firstDayOfWeek` and `firstWeekOfYear` (`GetWeekNumberOptions`; ISO 8601 is `{ firstDayOfWeek: 1, firstWeekOfYear: 'first-four-day-week' }`). With the types `DayOfWeek` (`0`–`6`, Sunday first) and `FirstWeekOfYear`. Server-safe. |
2188
+ | `buttonClassNames`, `toastTitleClassNames`, … | The stable class names of every component and part (see [Styling hooks](#styling-hooks)). Server-safe. |
2189
+ | `cn(...classes)` | clsx + tailwind-merge that knows Wave's type ramp and shadows; the last class wins over a conflicting class of the same variant. |
2190
+ | `composeEventHandlers(theirs, ours)` | Run a consumer handler, then yours unless it called `preventDefault()`. |
2191
+ | `mergeRefs(...refs)` | Merge refs outside components. |
2192
+ | `resolveSlot`, `renderSlot` | The slot helpers the components use. |
2193
+ | `getThemeClassName(theme)` | The theme classes of a theme. |
2194
+
2195
+ ## Upgrading from 0.10
2196
+
2197
+ 0.11 removes nothing public; these changes can still affect your tests, styles and types:
2198
+
2199
+ 1. **Dialog and Drawer backdrops are the surface's siblings.** A test or a style that reached the backdrop through `dialog.parentElement` or `panel.parentElement` uses `.wave-dialog-content__backdrop` or `.wave-drawer__backdrop` (the Dialog surface now sits in `.wave-dialog-content__positioner`).
2200
+ 2. **Dialog and Drawer mount through the presence core:** their elements carry `data-presence`, and a closed surface without motion unmounts one render after the closing commit (in the same `act()`). A test that queries a closed kept-mounted surface (`unmountOnClose={false}`) passes `{ hidden: true }`.
2201
+ 3. **A modal opened over another modal no longer dims the page twice:** its backdrop is transparent; `backdrop={{ appearance: 'dimmed' }}` keeps 0.10's look.
2202
+ 4. **Every Dialog and Drawer trigger toggles:** a trigger inside its own surface, or the trigger of a non-modal or inline surface, closes it when clicked while open. A render-prop `Drawer.Trigger` that reads `aria-haspopup` accepts `undefined` (inline drawers render none).
2203
+ 5. **Drawer layout:** a hand-made action row (or a `Dialog.Footer`) at the end of a Drawer's content becomes `Drawer.Footer` to stay in view; a `title` or `titleAction` next to a new `Drawer.Header` is ignored (move it into a `Drawer.HeaderTitle`). A test that asserts `pe-8` on a Dialog title asserts `pe-[var(--wave-dialog-title-end-padding,2rem)]`.
2204
+ 6. **`ModalType` gains `'non-modal'` and `DrawerPosition` `'bottom'`:** an exhaustive `switch` or a `Record` over either needs the case.
2205
+ 7. **Tag:** a focused Dismiss button dismisses on Delete and Backspace, also outside a TagGroup. Assertions on Tag's exact class string or padding classes change (`ps-3` → `ps-2.75`, `pe-1.5` → `pe-1.25`, `pe-3` → `pe-2.75`, `py-1` → `min-h-7`; `toHaveClass('bg-muted')` does not). Tag's new own props shadow the attributes of the same names: `as="li" value={3}` becomes `value={String(3)}`, and `size`, `shape`, `appearance`, `selected`, `media`, `icon` and `secondaryText` no longer reach an `as` component; an interface that extends `TagProps` and declares one of them with another type no longer compiles. A dismissible Tag rendered as a button or a link (`as="button"`, `as="a"`, a link component) warns `Tag:nested-button` in development: use InteractionTag for a button, and put a link in the tag's content (`<Tag dismissible><a href>…</a></Tag>`).
2206
+ 8. **TagPicker:** its tags are Tags in a TagGroup, so tests that assert the 0.10 tag classes (`px-2 py-0.5 text-body-1`), the remove button's `ms-0.5 rounded-sm` or its 12px glyph, query `[data-wave-tagpicker-remove]` or count the picker's buttons (one more: "Show options") change; `expandIcon={false}` keeps the 0.10 field. After a keyboard removal focus is on the next tag, not on the input; a press on the empty area of an open TagPicker closes the list. In the parts form, keep the options of selected values in `TagPicker.List`: TagPicker hides them and reads their labels.
2207
+ 9. **`import { TagPickerOption }`** now brings the component (`TagPicker.Option`) and the entry type; `import { type TagPickerOption }` keeps only the type.
2208
+ 10. **Nav:** a Nav whose list holds content that is not an `<li>` (a Divider, a heading, a `<div>`, a Tooltip wrapped around a `Nav.Item`) logs a development warning; use `Nav.Divider` or `Nav.SectionHeader`, or wrap the content in an `<li>`. A test that spies on `console.warn` around such a Nav sees the message.
2209
+
2210
+ The CHANGELOG's [0.11.0 "Changed" section](CHANGELOG.md#changed) lists every behaviour, DOM and type change.
2211
+
2212
+ ## Upgrading from 0.9
2213
+
2214
+ 0.10 removes nothing public; these changes can still affect your tests and types:
2215
+
2216
+ 1. DatePicker's calendar heading is a button (the month picker, on by default): the calendar dialog gains a Tab stop between the previous-month and next-month buttons, so tests that count Tab presses or list the dialog's tabbable elements change. Queries by the heading and the names of the dialog and the grid are unchanged. Escape in the month or year view goes back a view and keeps the dialog open; only Escape in the day view closes it. `showMonthPicker={false}` keeps a static heading.
2217
+ 2. The development error of an `Option` outside a listbox names every listbox: `… must be used within a listbox (Listbox, Combobox, Dropdown or a ListboxProvider)`.
2218
+ 3. `OptionGroup`'s `aria-label` and `aria-labelledby` go to its `role="group"` list, which they name; 0.9 put them on the group's `role="presentation"` item.
2219
+ 4. SwatchPicker renders `data-layout`, `data-focus-mode`, `data-size`, `data-shape` and `data-spacing` on its root, and the swatches of `items` carry `wave-color-swatch` next to `wave-swatch-picker__swatch`: assertions on the root's exact attributes or a swatch's complete class string change.
2220
+ 5. The popups of Dropdown, Combobox and TagPicker carry `wave-listbox-surface` and `wave-listbox-surface__list` next to their own stable classes: assertions on a complete class string change.
2221
+ 6. `SwatchPickerSize` gains `'extra-small'`: a `Record<SwatchPickerSize, …>` of yours needs the key.
2222
+ 7. A multi-select Dropdown or Combobox needs a literal `multiselect`: `multiselect={flag}` with a `boolean` variable matches neither call signature of the component, so render two elements, one for each mode.
2223
+
2224
+ The CHANGELOG's [0.10.0 "Changed" section](CHANGELOG.md#changed-1) lists every behaviour, DOM and type change.
1619
2225
 
1620
2226
  ## Upgrading from 0.8
1621
2227
 
@@ -1630,7 +2236,7 @@ Typeahead (Menu, selectable List, Tree, Dropdown) accepts characters typed with
1630
2236
  7. `Rating` and `RatingDisplay` `color` is a `RatingColor` (`'neutral' | 'brand' | 'marigold'`): it narrows the `color?: string` inherited from `React.HTMLAttributes`. Both gain the static member `Item` and render their `children` (0.7 ignored them).
1631
2237
  8. `WaveContextValue` (the result of `useWaveTheme()`) gains `inputDefaults`: code that builds one (a test double) adds it. `InputProps`, `SelectProps` and `SliderProps` declare their own `size`, and `SpinButtonProps` gains `allowEmpty?: false` next to the new `SpinButtonAllowEmptyProps`.
1632
2238
 
1633
- The CHANGELOG's [0.9.0 "Upgrading from 0.8"](CHANGELOG.md#upgrading-from-08) and [0.9.0 "Changed" section](CHANGELOG.md#changed) list every behaviour, DOM, visual and type change.
2239
+ The CHANGELOG's [0.9.0 "Upgrading from 0.8"](CHANGELOG.md#upgrading-from-08) and [0.9.0 "Changed" section](CHANGELOG.md#changed-2) list every behaviour, DOM, visual and type change.
1634
2240
 
1635
2241
  ## Upgrading from 0.7
1636
2242
 
@@ -1648,7 +2254,7 @@ The CHANGELOG's [0.9.0 "Upgrading from 0.8"](CHANGELOG.md#upgrading-from-08) and
1648
2254
  10. `ToastController['dispatchToast']` is overloaded: a test double that implements it must accept content too.
1649
2255
  11. A `dispatchToast` options argument that is not a plain object (a class instance, `Object.create(defaults)`) is now read as React content and throws when it renders: pass a plain object (spread it: `{ ...defaults, title }`).
1650
2256
 
1651
- The CHANGELOG's [0.8.0 "Changed" section](CHANGELOG.md#changed-1) lists every behaviour, DOM and type change.
2257
+ The CHANGELOG's [0.8.0 "Changed" section](CHANGELOG.md#changed-3) lists every behaviour, DOM and type change.
1652
2258
 
1653
2259
  ## Upgrading from 0.6
1654
2260
 
@@ -1664,7 +2270,7 @@ The CHANGELOG's [0.8.0 "Changed" section](CHANGELOG.md#changed-1) lists every be
1664
2270
  8. Wave now requires tailwind-merge 3.7, so an axis shorthand in your `className` (`px-6`) replaces a component's own logical side class of the same variant (`pe-8` on `Dialog.Title`) instead of losing to it in the CSS: write `px-6 pe-8` to keep the side.
1665
2271
  9. Wave requires Node.js 22.12 or later (`engines`; Node.js 20 reached its end of life on 2026-04-30). npm and pnpm warn on an older Node.js (npm stops with `engine-strict`), and Yarn 1 refuses to install.
1666
2272
 
1667
- The CHANGELOG's [0.7.0 "Changed" section](CHANGELOG.md#changed-2) lists every behaviour, DOM and type change.
2273
+ The CHANGELOG's [0.7.0 "Changed" section](CHANGELOG.md#changed-4) lists every behaviour, DOM and type change.
1668
2274
 
1669
2275
  ## Upgrading from 0.5
1670
2276
 
@@ -1679,11 +2285,11 @@ The CHANGELOG's [0.7.0 "Changed" section](CHANGELOG.md#changed-2) lists every be
1679
2285
  7. RatingDisplay formats its accessible name with the runtime locale when `locale` is omitted: pass `locale` when rendering on the server.
1680
2286
  8. Badge `color="important"` becomes a neutral color in 1.0: switch to `color="severe"` to keep the orange look.
1681
2287
 
1682
- The CHANGELOG's [0.6.0 "Changed" section](CHANGELOG.md#changed-3) lists every behaviour, DOM and type change.
2288
+ The CHANGELOG's [0.6.0 "Changed" section](CHANGELOG.md#changed-5) lists every behaviour, DOM and type change.
1683
2289
 
1684
2290
  ## Upgrading from 0.4
1685
2291
 
1686
- npm went from 0.4.0 to 0.7.0: 0.5.0 and 0.6.0 were never published, and their changes ship in 0.7.0. From 0.4.0, take these steps, then those of [Upgrading from 0.5](#upgrading-from-05), [Upgrading from 0.6](#upgrading-from-06), [Upgrading from 0.7](#upgrading-from-07) and, for 0.9, [Upgrading from 0.8](#upgrading-from-08).
2292
+ npm went from 0.4.0 to 0.7.0: 0.5.0 and 0.6.0 were never published, and their changes ship in 0.7.0. From 0.4.0, take these steps, then those of [Upgrading from 0.5](#upgrading-from-05), [Upgrading from 0.6](#upgrading-from-06), [Upgrading from 0.7](#upgrading-from-07), [Upgrading from 0.8](#upgrading-from-08) and [Upgrading from 0.9](#upgrading-from-09).
1687
2293
 
1688
2294
  1. The npm package name is unchanged (`@mortenbrudvik/waveui`, as in 0.4.0). Replace `waveui` imports copied from the 0.4 guide, and a git or local dependency named `waveui`, with `@mortenbrudvik/waveui`.
1689
2295
  2. Styles: without Tailwind, keep `import '@mortenbrudvik/waveui/styles'` and make sure a `WaveProvider` wraps the app; with Tailwind 4, switch to `@import '@mortenbrudvik/waveui/tailwind';` after `@import 'tailwindcss';`. Import `preflight.css` if you relied on Wave's Preflight.