@cdx-ui/components 0.2.3 → 0.2.4

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 (248) hide show
  1. package/AGENTS.md +24 -0
  2. package/README.md +2 -0
  3. package/coverage.svg +1 -1
  4. package/docs/components/accordion/content.md +538 -0
  5. package/docs/components/alert/content.md +203 -0
  6. package/docs/components/alert-dialog/content.md +319 -0
  7. package/docs/components/avatar/content.md +368 -0
  8. package/docs/components/avatar/design.md +202 -0
  9. package/docs/components/badge/content.md +235 -0
  10. package/docs/components/bottom-sheet/content.md +351 -0
  11. package/docs/components/bottom-sheet/design.md +249 -0
  12. package/docs/components/breadcrumbs/content.md +169 -0
  13. package/docs/components/button/content.md +398 -0
  14. package/docs/components/button/design.md +258 -0
  15. package/docs/components/calendar/content.md +137 -0
  16. package/docs/components/card/content.md +283 -0
  17. package/docs/components/carousel/content.md +416 -0
  18. package/docs/components/checkbox/content.md +236 -0
  19. package/docs/components/checkbox/design.md +214 -0
  20. package/docs/components/chip/content.md +439 -0
  21. package/docs/components/chip/design.md +418 -0
  22. package/docs/components/color-area/content.md +86 -0
  23. package/docs/components/color-field/content.md +134 -0
  24. package/docs/components/color-picker/content.md +208 -0
  25. package/docs/components/color-slider/content.md +120 -0
  26. package/docs/components/color-swatch/content.md +64 -0
  27. package/docs/components/color-swatch-picker/content.md +126 -0
  28. package/docs/components/content.md +97 -0
  29. package/docs/components/currency-input/content.md +205 -0
  30. package/docs/components/data-grid/content.md +522 -0
  31. package/docs/components/date-field/content.md +147 -0
  32. package/docs/components/date-picker/content.md +360 -0
  33. package/docs/components/date-range-picker/content.md +301 -0
  34. package/docs/components/date-time-field/content.md +157 -0
  35. package/docs/components/date-time-list/content.md +105 -0
  36. package/docs/components/date-time-picker/content.md +272 -0
  37. package/docs/components/dialog/content.md +436 -0
  38. package/docs/components/dialog/design.md +192 -0
  39. package/docs/components/divider/content.md +112 -0
  40. package/docs/components/drawer/content.md +436 -0
  41. package/docs/components/field/content.md +277 -0
  42. package/docs/components/form/content.md +62 -0
  43. package/docs/components/full-screen-dialog/content.md +881 -0
  44. package/docs/components/heading/content.md +55 -0
  45. package/docs/components/icon/content.md +190 -0
  46. package/docs/components/icon-button/content.md +200 -0
  47. package/docs/components/icon-button/design.md +165 -0
  48. package/docs/components/image/content.md +104 -0
  49. package/docs/components/input/content.md +281 -0
  50. package/docs/components/input/design.md +389 -0
  51. package/docs/components/insurance-disclosure/content.md +103 -0
  52. package/docs/components/link/content.md +180 -0
  53. package/docs/components/link/design.md +160 -0
  54. package/docs/components/list-item/content.md +695 -0
  55. package/docs/components/menu/content.md +1247 -0
  56. package/docs/components/month-calendar/content.md +84 -0
  57. package/docs/components/month-picker/content.md +273 -0
  58. package/docs/components/otp-input/content.md +327 -0
  59. package/docs/components/progress-bar/content.md +87 -0
  60. package/docs/components/progress-circle/content.md +176 -0
  61. package/docs/components/progress-segmented/content.md +119 -0
  62. package/docs/components/quick-action-button/content.md +154 -0
  63. package/docs/components/quick-action-button/design.md +170 -0
  64. package/docs/components/radio/content.md +224 -0
  65. package/docs/components/range-calendar/content.md +123 -0
  66. package/docs/components/reorderable-list/content.md +309 -0
  67. package/docs/components/scoped-theme/content.md +47 -0
  68. package/docs/components/scroll-shadow/content.md +686 -0
  69. package/docs/components/select/content.md +400 -0
  70. package/docs/components/select/design.md +301 -0
  71. package/docs/components/skeleton/content.md +103 -0
  72. package/docs/components/slider/content.md +423 -0
  73. package/docs/components/stack/content.md +161 -0
  74. package/docs/components/switch/content.md +150 -0
  75. package/docs/components/switch/design.md +208 -0
  76. package/docs/components/table/content.md +548 -0
  77. package/docs/components/table-pagination/content.md +177 -0
  78. package/docs/components/tabs/content.md +292 -0
  79. package/docs/components/text/content.md +142 -0
  80. package/docs/components/textarea/content.md +222 -0
  81. package/docs/components/tile/content.md +703 -0
  82. package/docs/components/toast/content.md +456 -0
  83. package/docs/components/toggle-button/content.md +505 -0
  84. package/docs/components/tooltip/content.md +350 -0
  85. package/docs/components/virtualized-list/content.md +250 -0
  86. package/docs/foundations/content.md +9 -0
  87. package/docs/foundations/expressions/icons/content.md +19 -0
  88. package/docs/foundations/styles/tokens/all-tokens/content.md +192 -0
  89. package/docs/foundations/styles/tokens/what-are-tokens/content.md +33 -0
  90. package/docs/foundations/styles/tokens/what-are-tokens/design.md +33 -0
  91. package/docs/getting-started/content.md +4 -0
  92. package/docs/getting-started/designers/content.md +13 -0
  93. package/docs/getting-started/designers/design.md +13 -0
  94. package/docs/getting-started/developers/accessibility/content.md +31 -0
  95. package/docs/getting-started/developers/content.md +612 -0
  96. package/docs/getting-started/developers/styling/content.md +193 -0
  97. package/docs/llms.txt +193 -0
  98. package/docs/patterns/account-management/content.md +75 -0
  99. package/docs/patterns/authentication/content.md +52 -0
  100. package/docs/patterns/content.md +10 -0
  101. package/docs/patterns/dashboard/content.md +39 -0
  102. package/docs/patterns/empty-states/content.md +31 -0
  103. package/docs/patterns/forms/content.md +109 -0
  104. package/docs/patterns/onboarding/content.md +69 -0
  105. package/docs/patterns/payments/content.md +35 -0
  106. package/docs/patterns/settings/content.md +72 -0
  107. package/docs/resources/content.md +9 -0
  108. package/docs/resources/figma-libraries/content.md +42 -0
  109. package/docs/resources/release-notes/0.0.1-beta.118/content.md +27 -0
  110. package/docs/resources/release-notes/0.0.1-beta.122/content.md +21 -0
  111. package/docs/resources/release-notes/0.0.1-beta.124/content.md +16 -0
  112. package/docs/resources/release-notes/0.0.1-beta.126/content.md +10 -0
  113. package/docs/resources/release-notes/0.0.1-beta.127/content.md +14 -0
  114. package/docs/resources/release-notes/0.0.1-beta.128/content.md +9 -0
  115. package/docs/resources/release-notes/0.0.1-beta.129/content.md +18 -0
  116. package/docs/resources/release-notes/0.0.1-beta.130/content.md +18 -0
  117. package/docs/resources/release-notes/0.0.1-beta.133/content.md +7 -0
  118. package/docs/resources/release-notes/0.0.1-beta.134/content.md +17 -0
  119. package/docs/resources/release-notes/0.0.1-beta.136/content.md +7 -0
  120. package/docs/resources/release-notes/0.0.1-beta.137/content.md +10 -0
  121. package/docs/resources/release-notes/0.0.1-beta.138/content.md +7 -0
  122. package/docs/resources/release-notes/0.0.1-beta.139/content.md +13 -0
  123. package/docs/resources/release-notes/0.0.1-beta.140/content.md +26 -0
  124. package/docs/resources/release-notes/0.0.1-beta.141/content.md +63 -0
  125. package/docs/resources/release-notes/0.0.1-beta.142/content.md +7 -0
  126. package/docs/resources/release-notes/0.0.1-beta.143/content.md +12 -0
  127. package/docs/resources/release-notes/0.0.1-beta.144/content.md +7 -0
  128. package/docs/resources/release-notes/0.0.1-beta.145/content.md +7 -0
  129. package/docs/resources/release-notes/0.0.1-beta.147/content.md +10 -0
  130. package/docs/resources/release-notes/0.0.1-beta.149/content.md +10 -0
  131. package/docs/resources/release-notes/0.0.1-beta.150/content.md +13 -0
  132. package/docs/resources/release-notes/0.0.1-beta.152/content.md +7 -0
  133. package/docs/resources/release-notes/0.0.1-beta.154/content.md +15 -0
  134. package/docs/resources/release-notes/0.0.1-beta.156/content.md +7 -0
  135. package/docs/resources/release-notes/0.0.1-beta.158/content.md +9 -0
  136. package/docs/resources/release-notes/0.0.1-beta.159/content.md +11 -0
  137. package/docs/resources/release-notes/0.0.1-beta.160/content.md +7 -0
  138. package/docs/resources/release-notes/0.0.1-beta.161/content.md +7 -0
  139. package/docs/resources/release-notes/0.0.1-beta.163/content.md +7 -0
  140. package/docs/resources/release-notes/0.0.1-beta.164/content.md +9 -0
  141. package/docs/resources/release-notes/0.0.1-beta.166/content.md +17 -0
  142. package/docs/resources/release-notes/0.0.1-beta.167/content.md +7 -0
  143. package/docs/resources/release-notes/0.0.1-beta.168/content.md +7 -0
  144. package/docs/resources/release-notes/0.0.1-beta.169/content.md +7 -0
  145. package/docs/resources/release-notes/0.0.1-beta.171/content.md +16 -0
  146. package/docs/resources/release-notes/0.0.1-beta.172/content.md +24 -0
  147. package/docs/resources/release-notes/0.0.1-beta.173/content.md +15 -0
  148. package/docs/resources/release-notes/0.0.1-beta.174/content.md +7 -0
  149. package/docs/resources/release-notes/0.0.1-beta.176/content.md +32 -0
  150. package/docs/resources/release-notes/0.0.1-beta.179/content.md +11 -0
  151. package/docs/resources/release-notes/0.0.1-beta.182/content.md +13 -0
  152. package/docs/resources/release-notes/0.0.1-beta.185/content.md +7 -0
  153. package/docs/resources/release-notes/0.0.1-beta.187/content.md +27 -0
  154. package/docs/resources/release-notes/0.1.1/content.md +23 -0
  155. package/docs/resources/release-notes/0.1.2/content.md +7 -0
  156. package/docs/resources/release-notes/0.1.4/content.md +19 -0
  157. package/docs/resources/release-notes/0.1.5/content.md +7 -0
  158. package/docs/resources/release-notes/0.1.6/content.md +7 -0
  159. package/docs/resources/release-notes/0.2.0/content.md +31 -0
  160. package/docs/resources/release-notes/0.2.1/content.md +17 -0
  161. package/docs/resources/release-notes/0.2.2/content.md +11 -0
  162. package/docs/resources/release-notes/0.2.3/content.md +15 -0
  163. package/docs/resources/release-notes/content.md +64 -0
  164. package/lib/commonjs/components/DateTimeList/index.js +11 -26
  165. package/lib/commonjs/components/DateTimeList/index.js.map +1 -1
  166. package/lib/commonjs/components/DateTimeList/styles.js +2 -12
  167. package/lib/commonjs/components/DateTimeList/styles.js.map +1 -1
  168. package/lib/commonjs/components/ListItem/index.js.map +1 -1
  169. package/lib/commonjs/components/ListItem/styles.js +2 -1
  170. package/lib/commonjs/components/ListItem/styles.js.map +1 -1
  171. package/lib/commonjs/components/Menu/index.js +11 -5
  172. package/lib/commonjs/components/Menu/index.js.map +1 -1
  173. package/lib/commonjs/components/ScrollShadow/gradient.js +29 -0
  174. package/lib/commonjs/components/ScrollShadow/gradient.js.map +1 -0
  175. package/lib/commonjs/components/ScrollShadow/index.js +171 -44
  176. package/lib/commonjs/components/ScrollShadow/index.js.map +1 -1
  177. package/lib/commonjs/components/ScrollShadow/styles.js +58 -14
  178. package/lib/commonjs/components/ScrollShadow/styles.js.map +1 -1
  179. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js +63 -0
  180. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  181. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +48 -8
  182. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
  183. package/lib/commonjs/components/Tabs/index.js +16 -23
  184. package/lib/commonjs/components/Tabs/index.js.map +1 -1
  185. package/lib/commonjs/components/Tabs/styles.js +1 -10
  186. package/lib/commonjs/components/Tabs/styles.js.map +1 -1
  187. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js +20 -5
  188. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js.map +1 -1
  189. package/lib/module/components/DateTimeList/index.js +12 -27
  190. package/lib/module/components/DateTimeList/index.js.map +1 -1
  191. package/lib/module/components/DateTimeList/styles.js +1 -11
  192. package/lib/module/components/DateTimeList/styles.js.map +1 -1
  193. package/lib/module/components/ListItem/index.js.map +1 -1
  194. package/lib/module/components/ListItem/styles.js +2 -1
  195. package/lib/module/components/ListItem/styles.js.map +1 -1
  196. package/lib/module/components/Menu/index.js +11 -5
  197. package/lib/module/components/Menu/index.js.map +1 -1
  198. package/lib/module/components/ScrollShadow/gradient.js +24 -0
  199. package/lib/module/components/ScrollShadow/gradient.js.map +1 -0
  200. package/lib/module/components/ScrollShadow/index.js +170 -48
  201. package/lib/module/components/ScrollShadow/index.js.map +1 -1
  202. package/lib/module/components/ScrollShadow/styles.js +57 -13
  203. package/lib/module/components/ScrollShadow/styles.js.map +1 -1
  204. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js +57 -0
  205. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  206. package/lib/module/components/ScrollShadow/useScrollShadow.js +46 -8
  207. package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
  208. package/lib/module/components/Tabs/index.js +17 -24
  209. package/lib/module/components/Tabs/index.js.map +1 -1
  210. package/lib/module/components/Tabs/styles.js +0 -9
  211. package/lib/module/components/Tabs/styles.js.map +1 -1
  212. package/lib/module/figma/ListItem.SectionHeader.figma.js +20 -5
  213. package/lib/module/figma/ListItem.SectionHeader.figma.js.map +1 -1
  214. package/lib/typescript/components/DateTimeList/index.d.ts.map +1 -1
  215. package/lib/typescript/components/DateTimeList/styles.d.ts +1 -4
  216. package/lib/typescript/components/DateTimeList/styles.d.ts.map +1 -1
  217. package/lib/typescript/components/ListItem/index.d.ts +5 -4
  218. package/lib/typescript/components/ListItem/index.d.ts.map +1 -1
  219. package/lib/typescript/components/ListItem/styles.d.ts +1 -1
  220. package/lib/typescript/components/ListItem/styles.d.ts.map +1 -1
  221. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  222. package/lib/typescript/components/ScrollShadow/gradient.d.ts +7 -0
  223. package/lib/typescript/components/ScrollShadow/gradient.d.ts.map +1 -0
  224. package/lib/typescript/components/ScrollShadow/index.d.ts +57 -15
  225. package/lib/typescript/components/ScrollShadow/index.d.ts.map +1 -1
  226. package/lib/typescript/components/ScrollShadow/styles.d.ts +8 -2
  227. package/lib/typescript/components/ScrollShadow/styles.d.ts.map +1 -1
  228. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts +13 -0
  229. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts.map +1 -0
  230. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts +15 -2
  231. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
  232. package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
  233. package/lib/typescript/components/Tabs/styles.d.ts +0 -7
  234. package/lib/typescript/components/Tabs/styles.d.ts.map +1 -1
  235. package/lib/typescript/figma/ListItem.SectionHeader.figma.d.ts.map +1 -1
  236. package/package.json +8 -6
  237. package/src/components/DateTimeList/index.tsx +13 -26
  238. package/src/components/DateTimeList/styles.ts +1 -10
  239. package/src/components/ListItem/index.tsx +5 -4
  240. package/src/components/ListItem/styles.ts +1 -0
  241. package/src/components/Menu/index.tsx +11 -8
  242. package/src/components/ScrollShadow/gradient.ts +48 -0
  243. package/src/components/ScrollShadow/index.tsx +300 -69
  244. package/src/components/ScrollShadow/styles.ts +45 -28
  245. package/src/components/ScrollShadow/useComposedScrollHandler.ts +58 -0
  246. package/src/components/ScrollShadow/useScrollShadow.ts +72 -10
  247. package/src/components/Tabs/index.tsx +25 -35
  248. package/src/components/Tabs/styles.ts +0 -9
@@ -0,0 +1,1247 @@
1
+ # Menu (https://design.candescent.com/docs/components/menu)
2
+
3
+ Resources: [Figma](https://www.figma.com/design/0lEVMVlLj8ZS4fgItr3pmd/%E2%AD%90-Forge-DS--Components?node-id=7606-560), [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/overlays-menu--default)
4
+
5
+ `Menu` combines an anchored overlay with a reusable `Menu.Content` list surface. The same items can be commands, checkbox or radio options, or custom layouts, either on the page or anchored to a trigger.
6
+
7
+ ```tsx
8
+ import { Menu } from '@cdx-ui/components';
9
+ ```
10
+
11
+ ## Basic
12
+
13
+ Use `Menu.Item` for one-off commands that perform an action immediately and do not retain a checked state, such as opening a transfer flow or downloading a statement.
14
+
15
+ ```tsx
16
+ import { useState } from 'react';
17
+ import { Box, Button, Menu, Text } from '@cdx-ui/components';
18
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
19
+
20
+ export default function MenuBasicDemo() {
21
+ const [result, setResult] = useState('Choose an account action');
22
+
23
+ return (
24
+ <Box className="items-start gap-2">
25
+ <Menu>
26
+ <Menu.Trigger asChild>
27
+ <Button color="action" variant="outline">
28
+ <Button.Label>Everyday Checking</Button.Label>
29
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
30
+ </Button>
31
+ </Menu.Trigger>
32
+ <Menu.Popup>
33
+ <Menu.Content accessibilityLabel="Everyday Checking actions">
34
+ <Menu.Item onPress={() => setResult('Opening account details')}>
35
+ <Menu.ItemLabel>View account details</Menu.ItemLabel>
36
+ </Menu.Item>
37
+ <Menu.Item onPress={() => setResult('Starting a transfer')}>
38
+ <Menu.ItemLabel>Transfer money</Menu.ItemLabel>
39
+ </Menu.Item>
40
+ <Menu.Item onPress={() => setResult('Preparing your latest statement')}>
41
+ <Menu.ItemLabel>Download statement</Menu.ItemLabel>
42
+ </Menu.Item>
43
+ <Menu.Separator />
44
+ <Menu.Item onPress={() => setResult('Opening alert preferences')}>
45
+ <Menu.ItemLabel>Manage alerts</Menu.ItemLabel>
46
+ </Menu.Item>
47
+ </Menu.Content>
48
+ </Menu.Popup>
49
+ </Menu>
50
+ <Text size="sm" color="secondary">
51
+ {result}
52
+ </Text>
53
+ </Box>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ## Checkbox items
59
+
60
+ Use `Menu.CheckboxItem` when each option can be enabled independently, and compose `Menu.ItemIndicator` for a trailing check. The menu stays open after each toggle.
61
+
62
+ ```tsx
63
+ import { useState } from 'react';
64
+ import { Button, Menu } from '@cdx-ui/components';
65
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
66
+
67
+ export default function MenuCheckboxItemsDemo() {
68
+ const [showBalances, setShowBalances] = useState(true);
69
+ const [includePending, setIncludePending] = useState(false);
70
+ const [maskAccountNumbers, setMaskAccountNumbers] = useState(true);
71
+ const enabledCount = [showBalances, includePending, maskAccountNumbers].filter(Boolean).length;
72
+
73
+ return (
74
+ <Menu>
75
+ <Menu.Trigger asChild>
76
+ <Button color="action" variant="outline">
77
+ <Button.Label>Account view · {enabledCount} enabled</Button.Label>
78
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
79
+ </Button>
80
+ </Menu.Trigger>
81
+ <Menu.Popup>
82
+ <Menu.Content accessibilityLabel="Account display preferences">
83
+ <Menu.CheckboxItem isChecked={showBalances} onChange={setShowBalances}>
84
+ <Menu.ItemLabel>Show available balances</Menu.ItemLabel>
85
+ <Menu.ItemIndicator />
86
+ </Menu.CheckboxItem>
87
+ <Menu.CheckboxItem isChecked={includePending} onChange={setIncludePending}>
88
+ <Menu.ItemLabel>Include pending transactions</Menu.ItemLabel>
89
+ <Menu.ItemIndicator />
90
+ </Menu.CheckboxItem>
91
+ <Menu.CheckboxItem isChecked={maskAccountNumbers} onChange={setMaskAccountNumbers}>
92
+ <Menu.ItemLabel>Mask account numbers</Menu.ItemLabel>
93
+ <Menu.ItemIndicator />
94
+ </Menu.CheckboxItem>
95
+ </Menu.Content>
96
+ </Menu.Popup>
97
+ </Menu>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ## Radio items
103
+
104
+ Use `Menu.RadioGroup` and `Menu.RadioItem` when exactly one option may be active, and compose `Menu.ItemIndicator` to mark the current choice. The trigger reflects that choice after the menu closes.
105
+
106
+ ```tsx
107
+ import { useState } from 'react';
108
+ import { Button, Menu } from '@cdx-ui/components';
109
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
110
+
111
+ const SORT_OPTIONS = [
112
+ { value: 'newest', label: 'Newest transactions first' },
113
+ { value: 'oldest', label: 'Oldest transactions first' },
114
+ { value: 'highest', label: 'Highest amount first' },
115
+ ] as const;
116
+
117
+ export default function MenuRadioItemsDemo() {
118
+ const [sortBy, setSortBy] = useState('newest');
119
+ const selectedLabel =
120
+ SORT_OPTIONS.find(({ value }) => value === sortBy)?.label ?? SORT_OPTIONS[0].label;
121
+
122
+ return (
123
+ <Menu>
124
+ <Menu.Trigger asChild>
125
+ <Button color="action" variant="outline">
126
+ <Button.Label>{selectedLabel}</Button.Label>
127
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
128
+ </Button>
129
+ </Menu.Trigger>
130
+ <Menu.Popup>
131
+ <Menu.Content accessibilityLabel="Transaction sort order">
132
+ <Menu.RadioGroup value={sortBy} onChange={setSortBy}>
133
+ {SORT_OPTIONS.map(({ label, value }) => (
134
+ <Menu.RadioItem key={value} value={value}>
135
+ <Menu.ItemLabel>{label}</Menu.ItemLabel>
136
+ <Menu.ItemIndicator />
137
+ </Menu.RadioItem>
138
+ ))}
139
+ </Menu.RadioGroup>
140
+ </Menu.Content>
141
+ </Menu.Popup>
142
+ </Menu>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ## With icons
148
+
149
+ Render `Menu.Content` on its own when the list is already on the page. The same items work inside `Menu.Popup` when they should anchor to a trigger.
150
+
151
+ ```tsx
152
+ import { Menu } from '@cdx-ui/components';
153
+ import { ContentCopy, Edit, Person } from '@cdx-ui/icons';
154
+
155
+ export default function MenuWithIconsDemo() {
156
+ return (
157
+ <Menu>
158
+ <Menu.Content accessibilityLabel="Account menu">
159
+ <Menu.Item onPress={() => undefined}>
160
+ <Menu.ItemIcon as={Person} />
161
+ <Menu.ItemLabel>View profile</Menu.ItemLabel>
162
+ </Menu.Item>
163
+ <Menu.Item onPress={() => undefined}>
164
+ <Menu.ItemIcon as={Edit} />
165
+ <Menu.ItemLabel>Edit profile</Menu.ItemLabel>
166
+ </Menu.Item>
167
+ <Menu.Item onPress={() => undefined}>
168
+ <Menu.ItemIcon as={ContentCopy} />
169
+ <Menu.ItemLabel>Copy link</Menu.ItemLabel>
170
+ </Menu.Item>
171
+ </Menu.Content>
172
+ </Menu>
173
+ );
174
+ }
175
+ ```
176
+
177
+ ## With shortcuts
178
+
179
+ `Menu.ItemShortcut` is a visual hint for a keystroke. It does not register or handle that shortcut.
180
+
181
+ ```tsx
182
+ import { Menu } from '@cdx-ui/components';
183
+
184
+ export default function MenuWithShortcutsDemo() {
185
+ return (
186
+ <Menu.Content accessibilityLabel="Edit menu" className="w-56">
187
+ <Menu.Item onPress={() => undefined}>
188
+ <Menu.ItemLabel>Undo</Menu.ItemLabel>
189
+ <Menu.ItemShortcut>⌘Z</Menu.ItemShortcut>
190
+ </Menu.Item>
191
+ <Menu.Item onPress={() => undefined}>
192
+ <Menu.ItemLabel>Redo</Menu.ItemLabel>
193
+ <Menu.ItemShortcut>⇧⌘Z</Menu.ItemShortcut>
194
+ </Menu.Item>
195
+ <Menu.Item onPress={() => undefined}>
196
+ <Menu.ItemLabel>Copy</Menu.ItemLabel>
197
+ <Menu.ItemShortcut>⌘C</Menu.ItemShortcut>
198
+ </Menu.Item>
199
+ </Menu.Content>
200
+ );
201
+ }
202
+ ```
203
+
204
+ ## Destructive
205
+
206
+ Set `color="danger"` on the action that removes or irreversibly changes something.
207
+
208
+ ```tsx
209
+ import { Menu } from '@cdx-ui/components';
210
+ import { Delete, Edit } from '@cdx-ui/icons';
211
+
212
+ export default function MenuDestructiveDemo() {
213
+ return (
214
+ <Menu.Content accessibilityLabel="Account actions" className="w-56">
215
+ <Menu.Item onPress={() => undefined}>
216
+ <Menu.ItemIcon as={Edit} />
217
+ <Menu.ItemLabel>Edit nickname</Menu.ItemLabel>
218
+ </Menu.Item>
219
+ <Menu.Separator />
220
+ <Menu.Item color="danger" onPress={() => undefined}>
221
+ <Menu.ItemIcon as={Delete} />
222
+ <Menu.ItemLabel>Delete account</Menu.ItemLabel>
223
+ </Menu.Item>
224
+ </Menu.Content>
225
+ );
226
+ }
227
+ ```
228
+
229
+ ## Sections
230
+
231
+ Use `Menu.SectionLabel` to name a group and `Menu.Separator` to divide groups. Neither is a menu item, so arrow keys skip them.
232
+
233
+ ```tsx
234
+ import { Menu } from '@cdx-ui/components';
235
+
236
+ export default function MenuSectionsDemo() {
237
+ return (
238
+ <Menu.Content accessibilityLabel="Account filters" className="w-56">
239
+ <Menu.SectionLabel>Status</Menu.SectionLabel>
240
+ <Menu.Item onPress={() => undefined}>
241
+ <Menu.ItemLabel>Active</Menu.ItemLabel>
242
+ </Menu.Item>
243
+ <Menu.Item onPress={() => undefined}>
244
+ <Menu.ItemLabel>Pending</Menu.ItemLabel>
245
+ </Menu.Item>
246
+ <Menu.Separator />
247
+ <Menu.SectionLabel>Type</Menu.SectionLabel>
248
+ <Menu.Item onPress={() => undefined}>
249
+ <Menu.ItemLabel>Checking</Menu.ItemLabel>
250
+ </Menu.Item>
251
+ <Menu.Item onPress={() => undefined}>
252
+ <Menu.ItemLabel>Savings</Menu.ItemLabel>
253
+ </Menu.Item>
254
+ </Menu.Content>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ## Inset
260
+
261
+ Set `inset` on items without an icon so their labels line up with icon items in the same menu.
262
+
263
+ ```tsx
264
+ import { Menu } from '@cdx-ui/components';
265
+ import { Edit, Person } from '@cdx-ui/icons';
266
+
267
+ export default function MenuInsetDemo() {
268
+ return (
269
+ <Menu.Content accessibilityLabel="Profile actions" className="w-56">
270
+ <Menu.Item onPress={() => undefined}>
271
+ <Menu.ItemIcon as={Person} />
272
+ <Menu.ItemLabel>View profile</Menu.ItemLabel>
273
+ </Menu.Item>
274
+ <Menu.Item inset onPress={() => undefined}>
275
+ <Menu.ItemLabel>Account settings</Menu.ItemLabel>
276
+ </Menu.Item>
277
+ <Menu.Item onPress={() => undefined}>
278
+ <Menu.ItemIcon as={Edit} />
279
+ <Menu.ItemLabel>Edit profile</Menu.ItemLabel>
280
+ </Menu.Item>
281
+ </Menu.Content>
282
+ );
283
+ }
284
+ ```
285
+
286
+ ## Group header
287
+
288
+ `Menu.GroupHeader` is a non-command row. Pair it with `Menu.ItemAction` when that row needs a button, such as clearing a selection. Arrow keys skip the header, and the button is its own tab stop.
289
+
290
+ ```tsx
291
+ import { useState } from 'react';
292
+ import { Button, Menu } from '@cdx-ui/components';
293
+
294
+ export default function MenuGroupHeaderDemo() {
295
+ const [showPending, setShowPending] = useState(true);
296
+
297
+ return (
298
+ <Menu.Content accessibilityLabel="Transaction filters" className="w-72">
299
+ <Menu.GroupHeader>
300
+ <Menu.ItemLabel>Filter by</Menu.ItemLabel>
301
+ <Menu.ItemAction>
302
+ <Button variant="ghost" color="action" size="small" onPress={() => setShowPending(false)}>
303
+ <Button.Label>Clear</Button.Label>
304
+ </Button>
305
+ </Menu.ItemAction>
306
+ </Menu.GroupHeader>
307
+ <Menu.Separator />
308
+ <Menu.CheckboxItem isChecked={showPending} onChange={setShowPending}>
309
+ <Menu.ItemControl />
310
+ <Menu.ItemLabel>Pending transactions</Menu.ItemLabel>
311
+ </Menu.CheckboxItem>
312
+ </Menu.Content>
313
+ );
314
+ }
315
+ ```
316
+
317
+ ## Item control
318
+
319
+ `Menu.ItemControl` draws presentational checkbox or radio chrome from the row's role. The row keeps the checked semantics, and the control is hidden from the accessibility tree. These rows stay open while the selection changes.
320
+
321
+ ```tsx
322
+ import { useState } from 'react';
323
+ import { Menu } from '@cdx-ui/components';
324
+
325
+ export default function MenuItemControlDemo() {
326
+ const [pending, setPending] = useState(true);
327
+ const [posted, setPosted] = useState(false);
328
+ const [status, setStatus] = useState('open');
329
+
330
+ return (
331
+ <Menu.Content accessibilityLabel="Transaction filters" className="w-64">
332
+ <Menu.SectionLabel>Activity</Menu.SectionLabel>
333
+ <Menu.CheckboxItem isChecked={pending} onChange={setPending}>
334
+ <Menu.ItemControl />
335
+ <Menu.ItemLabel>Pending</Menu.ItemLabel>
336
+ </Menu.CheckboxItem>
337
+ <Menu.CheckboxItem isChecked={posted} onChange={setPosted}>
338
+ <Menu.ItemControl />
339
+ <Menu.ItemLabel>Posted</Menu.ItemLabel>
340
+ </Menu.CheckboxItem>
341
+ <Menu.Separator />
342
+ <Menu.SectionLabel>Status</Menu.SectionLabel>
343
+ <Menu.RadioGroup value={status} onChange={setStatus}>
344
+ <Menu.RadioItem value="open">
345
+ <Menu.ItemControl />
346
+ <Menu.ItemLabel>Open</Menu.ItemLabel>
347
+ </Menu.RadioItem>
348
+ <Menu.RadioItem value="closed">
349
+ <Menu.ItemControl />
350
+ <Menu.ItemLabel>Closed</Menu.ItemLabel>
351
+ </Menu.RadioItem>
352
+ </Menu.RadioGroup>
353
+ </Menu.Content>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ## Item count
359
+
360
+ Nest `Menu.ItemCount` inside `Menu.ItemLabel` on a row that includes `Menu.ItemControl` so the count uses secondary text and wraps with the label.
361
+
362
+ ```tsx
363
+ import { useState } from 'react';
364
+ import { Menu } from '@cdx-ui/components';
365
+
366
+ const CATEGORIES = [
367
+ { id: 'admin', label: 'Administrative services', count: 21 },
368
+ { id: 'banking', label: 'Business banking & money movement', count: 10 },
369
+ ] as const;
370
+
371
+ export default function MenuItemCountDemo() {
372
+ const [categories, setCategories] = useState<Record<string, boolean>>({ admin: true });
373
+
374
+ return (
375
+ <Menu.Content accessibilityLabel="Categories" className="w-64">
376
+ {CATEGORIES.map((category) => (
377
+ <Menu.CheckboxItem
378
+ key={category.id}
379
+ isChecked={categories[category.id] ?? false}
380
+ onChange={(isChecked) =>
381
+ setCategories((current) => ({ ...current, [category.id]: isChecked }))
382
+ }
383
+ >
384
+ <Menu.ItemControl />
385
+ <Menu.ItemLabel>
386
+ {category.label} <Menu.ItemCount>{category.count}</Menu.ItemCount>
387
+ </Menu.ItemLabel>
388
+ </Menu.CheckboxItem>
389
+ ))}
390
+ </Menu.Content>
391
+ );
392
+ }
393
+ ```
394
+
395
+ ## Item action
396
+
397
+ Put a `Button` in `Menu.ItemAction` when a command row needs a trailing action. Pressing the button does not activate the row or dismiss the menu.
398
+
399
+ ```tsx
400
+ import { useState } from 'react';
401
+ import { Box, Button, Menu, Text } from '@cdx-ui/components';
402
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
403
+
404
+ export default function MenuItemActionDemo() {
405
+ const [result, setResult] = useState('Choose an account action');
406
+
407
+ return (
408
+ <Box className="items-start gap-2">
409
+ <Menu>
410
+ <Menu.Trigger asChild>
411
+ <Button color="action" variant="outline">
412
+ <Button.Label>Everyday Checking</Button.Label>
413
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
414
+ </Button>
415
+ </Menu.Trigger>
416
+ <Menu.Popup className="w-72">
417
+ <Menu.Content accessibilityLabel="Everyday Checking actions">
418
+ <Menu.Item onPress={() => setResult('Opening account details')}>
419
+ <Menu.ItemLabel>View details</Menu.ItemLabel>
420
+ <Menu.ItemAction>
421
+ <Button
422
+ variant="ghost"
423
+ color="action"
424
+ size="small"
425
+ onPress={() => setResult('Copied account number')}
426
+ >
427
+ <Button.Label>Copy</Button.Label>
428
+ </Button>
429
+ </Menu.ItemAction>
430
+ </Menu.Item>
431
+ <Menu.Item onPress={() => setResult('Starting a transfer')}>
432
+ <Menu.ItemLabel>Transfer money</Menu.ItemLabel>
433
+ </Menu.Item>
434
+ </Menu.Content>
435
+ </Menu.Popup>
436
+ </Menu>
437
+ <Text size="sm" color="secondary">
438
+ {result}
439
+ </Text>
440
+ </Box>
441
+ );
442
+ }
443
+ ```
444
+
445
+ ## With filters
446
+
447
+ Compose a group header, section labels, checkbox rows, and radio groups when customers change several filters before dismissing the menu.
448
+
449
+ ```tsx
450
+ import { useState } from 'react';
451
+ import { Button, Menu } from '@cdx-ui/components';
452
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
453
+
454
+ const CATEGORIES = [
455
+ { id: 'admin', label: 'Administrative services', count: 21 },
456
+ { id: 'banking', label: 'Business banking & money movement', count: 10 },
457
+ { id: 'growth', label: 'Engagement & growth', count: 5 },
458
+ { id: 'wellbeing', label: 'Financial wellbeing', count: 5 },
459
+ { id: 'security', label: 'Identity & business security', count: 5 },
460
+ { id: 'intelligence', label: 'Intelligence & connectivity', count: 1 },
461
+ { id: 'lending', label: 'Lending & credit', count: 3 },
462
+ ] as const;
463
+
464
+ const STATUSES = [
465
+ { id: 'all', label: 'All statuses', count: 51 },
466
+ { id: 'available', label: 'Available', count: 45 },
467
+ { id: 'pilot', label: 'Ready for pilot', count: 2 },
468
+ { id: 'soon', label: 'Coming soon', count: 3 },
469
+ ] as const;
470
+
471
+ const PROVIDERS = [
472
+ { id: 'all', label: 'All providers', count: 51 },
473
+ { id: 'partners', label: 'Partners', count: 36 },
474
+ { id: 'candescent', label: 'Candescent', count: 15 },
475
+ ] as const;
476
+
477
+ export default function MenuWithFiltersDemo() {
478
+ const [categories, setCategories] = useState<Record<string, boolean>>({
479
+ admin: true,
480
+ banking: false,
481
+ });
482
+ const [status, setStatus] = useState('available');
483
+ const [provider, setProvider] = useState('all');
484
+
485
+ const clearFilters = () => {
486
+ setCategories({});
487
+ setStatus('all');
488
+ setProvider('all');
489
+ };
490
+
491
+ return (
492
+ <Menu>
493
+ <Menu.Trigger asChild>
494
+ <Button color="action" variant="outline">
495
+ <Button.Label>Catalog filters</Button.Label>
496
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
497
+ </Button>
498
+ </Menu.Trigger>
499
+ <Menu.Popup className="w-72">
500
+ <Menu.Content accessibilityLabel="Catalog filters">
501
+ <Menu.GroupHeader>
502
+ <Menu.ItemLabel>Filter by</Menu.ItemLabel>
503
+ <Menu.ItemAction>
504
+ <Button variant="ghost" color="action" size="small" onPress={clearFilters}>
505
+ <Button.Label>Clear filters</Button.Label>
506
+ </Button>
507
+ </Menu.ItemAction>
508
+ </Menu.GroupHeader>
509
+ <Menu.Separator />
510
+ <Menu.SectionLabel>Categories</Menu.SectionLabel>
511
+ {CATEGORIES.map((category) => (
512
+ <Menu.CheckboxItem
513
+ key={category.id}
514
+ isChecked={categories[category.id] ?? false}
515
+ onChange={(isChecked) =>
516
+ setCategories((current) => ({ ...current, [category.id]: isChecked }))
517
+ }
518
+ >
519
+ <Menu.ItemControl />
520
+ <Menu.ItemLabel>
521
+ {category.label} <Menu.ItemCount>{category.count}</Menu.ItemCount>
522
+ </Menu.ItemLabel>
523
+ </Menu.CheckboxItem>
524
+ ))}
525
+ <Menu.Separator />
526
+ <Menu.SectionLabel>Status</Menu.SectionLabel>
527
+ <Menu.RadioGroup value={status} onChange={setStatus}>
528
+ {STATUSES.map((item) => (
529
+ <Menu.RadioItem key={item.id} value={item.id}>
530
+ <Menu.ItemControl />
531
+ <Menu.ItemLabel>
532
+ {item.label} <Menu.ItemCount>{item.count}</Menu.ItemCount>
533
+ </Menu.ItemLabel>
534
+ </Menu.RadioItem>
535
+ ))}
536
+ </Menu.RadioGroup>
537
+ <Menu.Separator />
538
+ <Menu.SectionLabel>Providers</Menu.SectionLabel>
539
+ <Menu.RadioGroup value={provider} onChange={setProvider}>
540
+ {PROVIDERS.map((item) => (
541
+ <Menu.RadioItem key={item.id} value={item.id}>
542
+ <Menu.ItemControl />
543
+ <Menu.ItemLabel>
544
+ {item.label} <Menu.ItemCount>{item.count}</Menu.ItemCount>
545
+ </Menu.ItemLabel>
546
+ </Menu.RadioItem>
547
+ ))}
548
+ </Menu.RadioGroup>
549
+ </Menu.Content>
550
+ </Menu.Popup>
551
+ </Menu>
552
+ );
553
+ }
554
+ ```
555
+
556
+ ## Scrollable
557
+
558
+ Use a scrollable radio menu when customers must choose one option from a longer set, such as a monthly statement period. The selected period persists, appears in the trigger, and remains checked when the menu reopens.
559
+
560
+ ```tsx
561
+ import { useState } from 'react';
562
+ import { Button, Menu } from '@cdx-ui/components';
563
+ import { CalendarMonth, KeyboardArrowDown } from '@cdx-ui/icons';
564
+
565
+ const STATEMENT_PERIODS = [
566
+ 'August 2026',
567
+ 'July 2026',
568
+ 'June 2026',
569
+ 'May 2026',
570
+ 'April 2026',
571
+ 'March 2026',
572
+ 'February 2026',
573
+ 'January 2026',
574
+ 'December 2025',
575
+ 'November 2025',
576
+ 'October 2025',
577
+ 'September 2025',
578
+ ] as const;
579
+
580
+ export default function MenuScrollableDemo() {
581
+ const [selectedPeriod, setSelectedPeriod] = useState<string>(STATEMENT_PERIODS[0]);
582
+
583
+ return (
584
+ <Menu>
585
+ <Menu.Trigger asChild>
586
+ <Button color="action" variant="outline">
587
+ <Button.Label>Statement · {selectedPeriod}</Button.Label>
588
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
589
+ </Button>
590
+ </Menu.Trigger>
591
+ <Menu.Popup>
592
+ <Menu.Content scrollable accessibilityLabel="Statement period">
593
+ <Menu.RadioGroup value={selectedPeriod} onChange={setSelectedPeriod}>
594
+ {STATEMENT_PERIODS.map((period) => (
595
+ <Menu.RadioItem key={period} value={period}>
596
+ <Menu.ItemIcon as={CalendarMonth} />
597
+ <Menu.ItemLabel>{period}</Menu.ItemLabel>
598
+ <Menu.ItemIndicator />
599
+ </Menu.RadioItem>
600
+ ))}
601
+ </Menu.RadioGroup>
602
+ </Menu.Content>
603
+ </Menu.Popup>
604
+ </Menu>
605
+ );
606
+ }
607
+ ```
608
+
609
+ ## Custom triggers
610
+
611
+ Use `asChild` to open a menu from any pressable component, such as a button, chip, or icon button.
612
+
613
+ ```tsx
614
+ import { Box, Button, Card, Chip, IconButton, Menu, Text } from '@cdx-ui/components';
615
+ import { AccountBalance, KeyboardArrowDown, MoreHoriz } from '@cdx-ui/icons';
616
+
617
+ export default function MenuCustomTriggersDemo() {
618
+ return (
619
+ <Box className="w-full max-w-sm gap-6">
620
+ <Box className="items-start gap-2">
621
+ <Text color="secondary" size="sm">
622
+ Money movement
623
+ </Text>
624
+ <Menu>
625
+ <Menu.Trigger asChild>
626
+ <Button color="action">
627
+ <Button.Label>Move money</Button.Label>
628
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
629
+ </Button>
630
+ </Menu.Trigger>
631
+ <Menu.Popup>
632
+ <Menu.Content accessibilityLabel="Move money">
633
+ <Menu.Item onPress={() => undefined}>
634
+ <Menu.ItemLabel>Transfer between accounts</Menu.ItemLabel>
635
+ </Menu.Item>
636
+ <Menu.Item onPress={() => undefined}>
637
+ <Menu.ItemLabel>Pay a bill</Menu.ItemLabel>
638
+ </Menu.Item>
639
+ <Menu.Item onPress={() => undefined}>
640
+ <Menu.ItemLabel>Send money</Menu.ItemLabel>
641
+ </Menu.Item>
642
+ </Menu.Content>
643
+ </Menu.Popup>
644
+ </Menu>
645
+ </Box>
646
+
647
+ <Box className="items-start gap-2">
648
+ <Text color="secondary" size="sm">
649
+ Account
650
+ </Text>
651
+ <Menu>
652
+ <Menu.Trigger asChild>
653
+ <Chip color="action" variant="outline">
654
+ <Chip.Icon as={AccountBalance} position="leading" />
655
+ <Chip.Label>Checking ••4821</Chip.Label>
656
+ <Chip.Icon as={KeyboardArrowDown} position="trailing" />
657
+ </Chip>
658
+ </Menu.Trigger>
659
+ <Menu.Popup>
660
+ <Menu.Content accessibilityLabel="Checking account actions">
661
+ <Menu.Item onPress={() => undefined}>
662
+ <Menu.ItemLabel>View account activity</Menu.ItemLabel>
663
+ </Menu.Item>
664
+ <Menu.Item onPress={() => undefined}>
665
+ <Menu.ItemLabel>Download statements</Menu.ItemLabel>
666
+ </Menu.Item>
667
+ <Menu.Item onPress={() => undefined}>
668
+ <Menu.ItemLabel>Manage alerts</Menu.ItemLabel>
669
+ </Menu.Item>
670
+ </Menu.Content>
671
+ </Menu.Popup>
672
+ </Menu>
673
+ </Box>
674
+
675
+ <Box className="gap-2">
676
+ <Text color="secondary" size="sm">
677
+ Card management
678
+ </Text>
679
+ <Card className="flex-row items-center justify-between px-2 py-1">
680
+ <Text>Cash Rewards ••1842</Text>
681
+ <Menu placement="bottom-end">
682
+ <Menu.Trigger asChild>
683
+ <IconButton
684
+ as={MoreHoriz}
685
+ variant="ghost"
686
+ size="small"
687
+ accessibilityLabel="Cash Rewards card actions"
688
+ />
689
+ </Menu.Trigger>
690
+ <Menu.Popup>
691
+ <Menu.Content accessibilityLabel="Cash Rewards card actions">
692
+ <Menu.Item onPress={() => undefined}>
693
+ <Menu.ItemLabel>Lock card</Menu.ItemLabel>
694
+ </Menu.Item>
695
+ <Menu.Item onPress={() => undefined}>
696
+ <Menu.ItemLabel>Replace card</Menu.ItemLabel>
697
+ </Menu.Item>
698
+ <Menu.Item color="danger" onPress={() => undefined}>
699
+ <Menu.ItemLabel>Report lost or stolen</Menu.ItemLabel>
700
+ </Menu.Item>
701
+ </Menu.Content>
702
+ </Menu.Popup>
703
+ </Menu>
704
+ </Card>
705
+ </Box>
706
+ </Box>
707
+ );
708
+ }
709
+ ```
710
+
711
+ ## Side placements
712
+
713
+ The default placement is `bottom-start`. Choose a side alone to center the popup, or append `-start` or `-end` to align it with the corresponding edge of the trigger.
714
+
715
+ ```tsx
716
+ import { Box, Button, Menu, Text, type MenuProps } from '@cdx-ui/components';
717
+
718
+ interface PlacementMenuProps {
719
+ placement: NonNullable<MenuProps['placement']>;
720
+ }
721
+
722
+ const PLACEMENT_GROUPS = [
723
+ {
724
+ label: 'Top',
725
+ placements: ['top-start', 'top', 'top-end'],
726
+ },
727
+ {
728
+ label: 'Right',
729
+ placements: ['right-start', 'right', 'right-end'],
730
+ },
731
+ {
732
+ label: 'Bottom',
733
+ placements: ['bottom-start', 'bottom', 'bottom-end'],
734
+ },
735
+ {
736
+ label: 'Left',
737
+ placements: ['left-start', 'left', 'left-end'],
738
+ },
739
+ ] as const satisfies readonly {
740
+ label: string;
741
+ placements: readonly PlacementMenuProps['placement'][];
742
+ }[];
743
+
744
+ function PlacementMenu({ placement }: PlacementMenuProps) {
745
+ return (
746
+ <Menu placement={placement}>
747
+ <Menu.Trigger asChild>
748
+ <Button variant="outline" color="action" size="small">
749
+ <Button.Label>{placement}</Button.Label>
750
+ </Button>
751
+ </Menu.Trigger>
752
+ <Menu.Popup>
753
+ <Menu.Content accessibilityLabel={`${placement} menu`}>
754
+ <Menu.Item onPress={() => undefined}>
755
+ <Menu.ItemLabel>View details</Menu.ItemLabel>
756
+ </Menu.Item>
757
+ <Menu.Item onPress={() => undefined}>
758
+ <Menu.ItemLabel>Copy link</Menu.ItemLabel>
759
+ </Menu.Item>
760
+ </Menu.Content>
761
+ </Menu.Popup>
762
+ </Menu>
763
+ );
764
+ }
765
+
766
+ export default function MenuPlacementDemo() {
767
+ return (
768
+ <Box className="w-140 gap-6">
769
+ {PLACEMENT_GROUPS.map(({ label, placements }) => (
770
+ <Box key={label} className="items-center gap-2">
771
+ <Text size="sm" color="secondary">
772
+ {label}
773
+ </Text>
774
+ <Box className="flex-row flex-wrap justify-center gap-2">
775
+ {placements.map((placement) => (
776
+ <PlacementMenu key={placement} placement={placement} />
777
+ ))}
778
+ </Box>
779
+ </Box>
780
+ ))}
781
+ </Box>
782
+ );
783
+ }
784
+ ```
785
+
786
+ The requested side is preferred rather than guaranteed. At viewport boundaries the popup flips to the opposite side when that side has more room, then shifts along the cross axis to remain within the default 8 px collision boundary.
787
+
788
+ ## Automatic placements
789
+
790
+ Use `auto` to select the fitting side with the most available space. `auto-start` and `auto-end` use the same side selection while preserving the requested alignment.
791
+
792
+ ```tsx
793
+ import { Box, Button, Menu, type MenuProps } from '@cdx-ui/components';
794
+
795
+ type AutomaticPlacement = Extract<
796
+ NonNullable<MenuProps['placement']>,
797
+ 'auto' | 'auto-start' | 'auto-end'
798
+ >;
799
+
800
+ const AUTOMATIC_PLACEMENTS = [
801
+ 'auto-start',
802
+ 'auto',
803
+ 'auto-end',
804
+ ] as const satisfies readonly AutomaticPlacement[];
805
+
806
+ function AutomaticPlacementMenu({ placement }: Readonly<{ placement: AutomaticPlacement }>) {
807
+ return (
808
+ <Menu placement={placement}>
809
+ <Menu.Trigger asChild>
810
+ <Button variant="outline" color="action" size="small">
811
+ <Button.Label>{placement}</Button.Label>
812
+ </Button>
813
+ </Menu.Trigger>
814
+ <Menu.Popup>
815
+ <Menu.Content accessibilityLabel={`${placement} menu`}>
816
+ <Menu.Item onPress={() => undefined}>
817
+ <Menu.ItemLabel>View details</Menu.ItemLabel>
818
+ </Menu.Item>
819
+ <Menu.Item onPress={() => undefined}>
820
+ <Menu.ItemLabel>Copy link</Menu.ItemLabel>
821
+ </Menu.Item>
822
+ </Menu.Content>
823
+ </Menu.Popup>
824
+ </Menu>
825
+ );
826
+ }
827
+
828
+ export default function MenuAutomaticPlacementDemo() {
829
+ return (
830
+ <Box className="w-140 flex-row flex-wrap justify-center gap-3 py-20">
831
+ {AUTOMATIC_PLACEMENTS.map((placement) => (
832
+ <AutomaticPlacementMenu key={placement} placement={placement} />
833
+ ))}
834
+ </Box>
835
+ );
836
+ }
837
+ ```
838
+
839
+ All placements use a 4 px `offset` between the trigger and popup and a `crossOffset` of 0 by default. `offset` changes the main-axis gap, while `crossOffset` moves the popup along the perpendicular axis.
840
+
841
+ ## Disabled
842
+
843
+ Set `disabled` to block activation and dim the item. Arrow keys skip disabled items.
844
+
845
+ ```tsx
846
+ import { Menu } from '@cdx-ui/components';
847
+
848
+ export default function MenuDisabledDemo() {
849
+ return (
850
+ <Menu.Content accessibilityLabel="Account actions" className="w-64">
851
+ <Menu.Item onPress={() => undefined}>
852
+ <Menu.ItemLabel>Edit</Menu.ItemLabel>
853
+ </Menu.Item>
854
+ <Menu.Item disabled onPress={() => undefined}>
855
+ <Menu.ItemLabel>Archive (unavailable)</Menu.ItemLabel>
856
+ </Menu.Item>
857
+ <Menu.Item color="danger" disabled onPress={() => undefined}>
858
+ <Menu.ItemLabel>Delete</Menu.ItemLabel>
859
+ </Menu.Item>
860
+ </Menu.Content>
861
+ );
862
+ }
863
+ ```
864
+
865
+ ## Custom items
866
+
867
+ Menu item variants accept arbitrary children and style overrides, so an action or selectable item can use a richer layout while retaining its press, keyboard, and accessibility behavior.
868
+
869
+ ```tsx
870
+ import { useState } from 'react';
871
+ import { Box, Button, Card, Chip, HStack, Menu, Text } from '@cdx-ui/components';
872
+ import { KeyboardArrowDown } from '@cdx-ui/icons';
873
+
874
+ const ACCOUNTS = [
875
+ {
876
+ id: 'checking',
877
+ name: 'Everyday Checking',
878
+ number: '••4821',
879
+ balance: '$5,248.10',
880
+ status: 'Primary',
881
+ statusColor: 'action',
882
+ },
883
+ {
884
+ id: 'savings',
885
+ name: 'High-Yield Savings',
886
+ number: '••1098',
887
+ balance: '$24,680.32',
888
+ status: '2.25% APY',
889
+ statusColor: 'success',
890
+ },
891
+ {
892
+ id: 'business',
893
+ name: 'Business Checking',
894
+ number: '••7315',
895
+ balance: '$12,904.76',
896
+ status: 'Business',
897
+ statusColor: 'info',
898
+ },
899
+ ] as const;
900
+
901
+ type AccountId = (typeof ACCOUNTS)[number]['id'];
902
+ type Account = (typeof ACCOUNTS)[number];
903
+
904
+ function AccountOption({ account }: Readonly<{ account: Account }>) {
905
+ return (
906
+ <Card className="flex-1">
907
+ <Card.Header>
908
+ <Box className="flex-1 gap-0.5">
909
+ <Text weight="semibold">{account.name}</Text>
910
+ <Text size="sm" color="secondary">
911
+ {account.number}
912
+ </Text>
913
+ </Box>
914
+ <Chip color={account.statusColor} size="small">
915
+ <Chip.Label>{account.status}</Chip.Label>
916
+ </Chip>
917
+ </Card.Header>
918
+ <Card.Content>
919
+ <HStack space="md" className="items-end justify-between">
920
+ <Text size="sm" color="secondary">
921
+ Available balance
922
+ </Text>
923
+ <Text weight="semibold">{account.balance}</Text>
924
+ </HStack>
925
+ </Card.Content>
926
+ </Card>
927
+ );
928
+ }
929
+
930
+ export default function MenuCustomItemsDemo() {
931
+ const [selectedAccountId, setSelectedAccountId] = useState<AccountId>('checking');
932
+ const selectedAccount = ACCOUNTS.find(({ id }) => id === selectedAccountId) ?? ACCOUNTS[0];
933
+
934
+ return (
935
+ <Menu>
936
+ <Menu.Trigger asChild>
937
+ <Button color="action" variant="outline">
938
+ <Button.Label>From {selectedAccount.name}</Button.Label>
939
+ <Button.Icon as={KeyboardArrowDown} position="trailing" />
940
+ </Button>
941
+ </Menu.Trigger>
942
+ <Menu.Popup className="w-80">
943
+ <Menu.Content accessibilityLabel="Funding account">
944
+ <Menu.RadioGroup
945
+ value={selectedAccountId}
946
+ onChange={(value) => setSelectedAccountId(value as AccountId)}
947
+ >
948
+ {ACCOUNTS.map((account) => (
949
+ <Menu.RadioItem
950
+ key={account.id}
951
+ value={account.id}
952
+ accessibilityLabel={`${account.name} ${account.number}, available balance ${account.balance}`}
953
+ >
954
+ <AccountOption account={account} />
955
+ </Menu.RadioItem>
956
+ ))}
957
+ </Menu.RadioGroup>
958
+ </Menu.Content>
959
+ </Menu.Popup>
960
+ </Menu>
961
+ );
962
+ }
963
+ ```
964
+
965
+ ## API
966
+
967
+ ### Menu
968
+
969
+ Root for an overlay menu. Controls open state, preferred placement, and offsets for `Menu.Trigger` and `Menu.Popup`; omit it when rendering `Menu.Content` as a standalone list surface.
970
+
971
+ | Prop | Type | Default | Description |
972
+ | --- | --- | --- | --- |
973
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the root's own styles. |
974
+ | `children?` | `ReactNode` | — | The `Menu.Trigger` and `Menu.Popup`. |
975
+ | `isOpen?` | `boolean` | — | Controlled open state. When set, `defaultOpen` has no effect. |
976
+ | `defaultOpen?` | `boolean` | `false` | Initial open state when uncontrolled. |
977
+ | `onOpenChange?` | `((open: boolean) => void)` | — | Called with the next open state when the menu opens or closes. |
978
+ | `disabled?` | `boolean` | `false` | Prevents the trigger from opening the menu. |
979
+ | `placement?` | `'auto' \| 'auto-start' \| 'auto-end' \| 'top' \| 'top-start' \| 'top-end' \| 'right' \| 'right-start' \| 'right-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end'` | — | Preferred popup placement relative to the trigger. |
980
+ | `offset?` | `number` | — | Distance between the trigger and popup in pixels. |
981
+ | `crossOffset?` | `number` | — | Cross-axis adjustment from the aligned popup position in pixels. |
982
+
983
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
984
+
985
+ ## Sub-components
986
+
987
+ ### Menu.Trigger
988
+
989
+ Pressable that opens and closes an overlay menu. Use `asChild` to merge trigger behavior onto an `IconButton`, `Button`, or another pressable element.
990
+
991
+ | Prop | Type | Default | Description |
992
+ | --- | --- | --- | --- |
993
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the trigger's own styles. |
994
+ | `children?` | `ReactNode` | — | Trigger content (or the element to render when `asChild` is set). |
995
+ | `asChild?` | `boolean` | — | Render the consumer's child element (for example a `Button`) as the trigger instead of the default `Pressable`. |
996
+
997
+ Inherits all [`PressableProps`](https://reactnative.dev/docs/pressable#props).
998
+
999
+ ### Menu.Popup
1000
+
1001
+ Cross-platform overlay container that escapes normal layout through a document-body portal on web and a transparent modal on native. It positions the menu around its trigger and handles outside-press dismissal.
1002
+
1003
+ | Prop | Type | Default | Description |
1004
+ | --- | --- | --- | --- |
1005
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the popup's own styles. |
1006
+ | `children?` | `ReactNode` | — | Popup content, typically a `Menu.Content`. |
1007
+
1008
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1009
+
1010
+ ### Menu.Content
1011
+
1012
+ Menu list surface. It can be rendered inside `Menu.Popup` or independently; overlay content scrolls automatically when constrained, while `scrollable` caps standalone surfaces. Use `asSurface` when another component owns the collection semantics.
1013
+
1014
+ | Prop | Type | Default | Description |
1015
+ | --- | --- | --- | --- |
1016
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the menu surface's own styles. |
1017
+ | `children?` | `ReactNode` | — | The menu's items, groups and separators. |
1018
+ | `scrollable?` | `boolean \| null` | `false` | Caps the surface height and scrolls its items. Content inside a `Menu.Popup` always scrolls. |
1019
+ | `onEscape?` | `(() => void)` | — | Called when Escape is pressed on web keyboard navigation. |
1020
+ | `asSurface?` | `boolean` | — | Visual-only surface without menu semantics (e.g. inside Select listbox). |
1021
+
1022
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1023
+
1024
+ ### Menu.Item
1025
+
1026
+ An individual pressable action. The `color` prop defaults to `neutral`; use `color="danger"` for destructive actions.
1027
+
1028
+ | Prop | Type | Default | Description |
1029
+ | --- | --- | --- | --- |
1030
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the item's own styles. |
1031
+ | `children?` | `ReactNode` | — | Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. |
1032
+ | `color?` | `'neutral' \| 'danger'` | `'neutral'` | Color theme applied to the item content and interaction states. |
1033
+ | `inset?` | `boolean \| null` | `false` | Indents the item content so its label lines up with items that have a leading icon. |
1034
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Pressable` host. |
1035
+ | `closeOnSelect?` | `boolean` | — | Whether activation closes an overlay Menu. Defaults to true for overlay items. |
1036
+
1037
+ Inherits all [`PressableProps`](https://reactnative.dev/docs/pressable#props).
1038
+
1039
+ ### Menu.CheckboxItem
1040
+
1041
+ A controllable or uncontrolled menu option that toggles independently. Compose `Menu.ItemIndicator` to display its checked state.
1042
+
1043
+ | Prop | Type | Default | Description |
1044
+ | --- | --- | --- | --- |
1045
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the item's own styles. |
1046
+ | `children?` | `ReactNode` | — | Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. |
1047
+ | `color?` | `'neutral' \| 'danger'` | `'neutral'` | Color theme applied to the item content and interaction states. |
1048
+ | `inset?` | `boolean \| null` | `false` | Indents the item content so its label lines up with items that have a leading icon. |
1049
+ | `isChecked?` | `boolean` | — | Controlled checked state. When set, `defaultIsChecked` has no effect. |
1050
+ | `closeOnSelect?` | `boolean` | — | Whether activation closes an overlay Menu. Defaults to true for overlay items. |
1051
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Pressable` host. |
1052
+ | `defaultIsChecked?` | `boolean` | — | Initial checked state when uncontrolled. |
1053
+ | `onChange?` | `((isChecked: boolean) => void)` | — | Called with the new checked state when the user toggles the item. |
1054
+
1055
+ Inherits all [`PressableProps`](https://reactnative.dev/docs/pressable#props).
1056
+
1057
+ ### Menu.RadioGroup
1058
+
1059
+ Coordinates exclusive selection across `Menu.RadioItem` children.
1060
+
1061
+ | Prop | Type | Default | Description |
1062
+ | --- | --- | --- | --- |
1063
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the group's own styles. |
1064
+ | `children?` | `ReactNode` | — | The `Menu.RadioItem` options in the group. |
1065
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `View` host. |
1066
+ | `value?` | `string` | — | Controlled `value` of the selected `Menu.RadioItem`. When set, `defaultValue` has no effect. |
1067
+ | `defaultValue?` | `string` | — | Initially selected `value` when uncontrolled. |
1068
+ | `onChange?` | `((value: string) => void)` | — | Called with the `value` of the item the user selects. |
1069
+
1070
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1071
+
1072
+ ### Menu.RadioItem
1073
+
1074
+ An exclusively selectable option within `Menu.RadioGroup`. Compose `Menu.ItemIndicator` to display its checked state.
1075
+
1076
+ | Prop | Type | Default | Description |
1077
+ | --- | --- | --- | --- |
1078
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the item's own styles. |
1079
+ | `children?` | `ReactNode` | — | Item content, typically a `Menu.ItemLabel` with optional icon, shortcut and indicator slots. |
1080
+ | `color?` | `'neutral' \| 'danger'` | `'neutral'` | Color theme applied to the item content and interaction states. |
1081
+ | `inset?` | `boolean \| null` | `false` | Indents the item content so its label lines up with items that have a leading icon. |
1082
+ | `closeOnSelect?` | `boolean` | — | Whether activation closes an overlay Menu. Defaults to true for overlay items. |
1083
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Pressable` host. |
1084
+ | `value` | `string` | — | Identifies this item within its `Menu.RadioGroup`. |
1085
+
1086
+ Inherits all [`PressableProps`](https://reactnative.dev/docs/pressable#props).
1087
+
1088
+ ### Menu.ItemLabel
1089
+
1090
+ Text label rendered inside a `Menu.Item`.
1091
+
1092
+ | Prop | Type | Default | Description |
1093
+ | --- | --- | --- | --- |
1094
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the label's own styles. |
1095
+ | `children?` | `ReactNode` | — | Label text. |
1096
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Text` host. |
1097
+ | `isChecked?` | `boolean` | — | Checked state when used outside a selectable Menu item (e.g. `Select.ItemLabel`). |
1098
+
1099
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
1100
+
1101
+ ### Menu.ItemIcon
1102
+
1103
+ Decorative Forge icon rendered inside a `Menu.Item`.
1104
+
1105
+ | Prop | Type | Default | Description |
1106
+ | --- | --- | --- | --- |
1107
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the icon's own styles. |
1108
+ | `children?` | `ReactNode` | — | The element to render when `asChild` is set. |
1109
+ | `filled?` | `boolean` | — | When `true`, renders the filled variant of the icon (if available). Defaults to `false` (outlined). |
1110
+ | `size?` | `string \| number` | — | Width and height of the icon, in pixels or any SVG length. The bare icon renders at 24 when omitted. |
1111
+ | `data-testid?` | `string` | — | Test identifier forwarded to the rendered SVG. |
1112
+ | `as?` | `ForgeIcon` | — | The `@cdx-ui/icons` component to render. |
1113
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the icon. |
1114
+ | `position?` | `'leading' \| 'trailing'` | `'leading'` | Side of the label the icon sits on; sets the spacing between them. |
1115
+
1116
+ Inherits all `ForgeIconProps` (react-native-svg `SvgProps` plus `filled` and `size`).
1117
+
1118
+ ### Menu.ItemIndicator
1119
+
1120
+ Decorative checked-state icon for `Menu.CheckboxItem` and `Menu.RadioItem`.
1121
+
1122
+ | Prop | Type | Default | Description |
1123
+ | --- | --- | --- | --- |
1124
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the indicator's own styles. |
1125
+ | `children?` | `ReactNode` | — | The element to render when `asChild` is set. |
1126
+ | `filled?` | `boolean` | — | When `true`, renders the filled variant of the icon (if available). Defaults to `false` (outlined). |
1127
+ | `size?` | `string \| number` | — | Width and height of the icon, in pixels or any SVG length. The bare icon renders at 24 when omitted. |
1128
+ | `data-testid?` | `string` | — | Test identifier forwarded to the rendered SVG. |
1129
+ | `as?` | `ForgeIcon` | `Check` | The `@cdx-ui/icons` component to render. |
1130
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the icon. |
1131
+ | `isChecked?` | `boolean` | — | Shows the indicator. Defaults to the checked state of the enclosing item. |
1132
+ | `position?` | `'leading' \| 'trailing'` | `'trailing'` | Side of the label the indicator sits on; sets the spacing between them. |
1133
+
1134
+ Inherits all `ForgeIconProps` (react-native-svg `SvgProps` plus `filled` and `size`).
1135
+
1136
+ ### Menu.ItemShortcut
1137
+
1138
+ Decorative text hint for a keyboard shortcut. This slot does not register or handle the shortcut.
1139
+
1140
+ | Prop | Type | Default | Description |
1141
+ | --- | --- | --- | --- |
1142
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the shortcut's own styles. |
1143
+ | `children?` | `ReactNode` | — | Shortcut hint text, such as `⌘C`. |
1144
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Text` host. |
1145
+
1146
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
1147
+
1148
+ ### Menu.GroupHeader
1149
+
1150
+ Non-interactive header row for filter panels. Host a trailing `Menu.ItemAction` button; this slot is not a command `menuitem` and is skipped by arrow-key roving.
1151
+
1152
+ | Prop | Type | Default | Description |
1153
+ | --- | --- | --- | --- |
1154
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the group header's own styles. |
1155
+ | `children?` | `ReactNode` | — | Header content, typically a `Menu.ItemLabel` and an optional `Menu.ItemAction`. |
1156
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `View` host. |
1157
+
1158
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1159
+
1160
+ ### Menu.ItemAction
1161
+
1162
+ Trailing action slot that isolates press from the parent row so a nested `Button` does not activate or dismiss the menu.
1163
+
1164
+ | Prop | Type | Default | Description |
1165
+ | --- | --- | --- | --- |
1166
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the action's own styles. |
1167
+ | `children?` | `ReactNode` | — | The trailing interactive control, such as a `Button`. |
1168
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `View` host. |
1169
+
1170
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1171
+
1172
+ ### Menu.ItemControl
1173
+
1174
+ Presentational Checkbox or Radio chrome for filter rows. Hidden from the accessibility tree; the parent checkbox or radio item owns checked semantics.
1175
+
1176
+ | Prop | Type | Default | Description |
1177
+ | --- | --- | --- | --- |
1178
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the control's own styles. |
1179
+ | `children?` | `ReactNode` | — | Not rendered; the control draws a checkbox or radio that mirrors the enclosing item. |
1180
+
1181
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1182
+
1183
+ ### Menu.ItemCount
1184
+
1185
+ Tertiary count rendered after a filter-row label. Nest it inside `Menu.ItemLabel` so it wraps with the text.
1186
+
1187
+ | Prop | Type | Default | Description |
1188
+ | --- | --- | --- | --- |
1189
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the count's own styles. |
1190
+ | `children?` | `ReactNode` | — | The count to display. |
1191
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Text` host. |
1192
+
1193
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
1194
+
1195
+ ### Menu.SectionLabel
1196
+
1197
+ Non-interactive heading used to label a group of related menu items.
1198
+
1199
+ | Prop | Type | Default | Description |
1200
+ | --- | --- | --- | --- |
1201
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the section label's own styles. |
1202
+ | `children?` | `ReactNode` | — | Section heading text. |
1203
+ | `asChild?` | `boolean` | — | Render the consumer's child element in place of the default `Text` host. |
1204
+
1205
+ Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
1206
+
1207
+ ### Menu.Separator
1208
+
1209
+ Non-interactive divider between groups of menu items.
1210
+
1211
+ | Prop | Type | Default | Description |
1212
+ | --- | --- | --- | --- |
1213
+ | `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the separator's own styles. |
1214
+ | `children?` | `ReactNode` | — | Not rendered; the separator draws its own line. |
1215
+
1216
+ Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
1217
+
1218
+ ## Anatomy
1219
+
1220
+ | `data-slot` | Element |
1221
+ | --------------------- | ----------------------------- |
1222
+ | `menu` | Overlay root |
1223
+ | `menu-trigger` | Overlay trigger |
1224
+ | `menu-popup` | Positioned overlay container |
1225
+ | `menu-content` | Menu list surface |
1226
+ | `menu-item` | Pressable action |
1227
+ | `menu-checkbox-item` | Independently checked option |
1228
+ | `menu-radio-group` | Exclusive option group |
1229
+ | `menu-radio-item` | Exclusively checked option |
1230
+ | `menu-item-label` | Item label |
1231
+ | `menu-item-icon` | Item icon |
1232
+ | `menu-item-shortcut` | Keyboard shortcut hint |
1233
+ | `menu-item-indicator` | Checked-state indicator |
1234
+ | `menu-item-action` | Trailing row action |
1235
+ | `menu-item-control` | Presentational filter control |
1236
+ | `menu-item-count` | Filter-row count |
1237
+ | `menu-group-header` | Non-command header row |
1238
+ | `menu-section-label` | Section heading |
1239
+ | `menu-separator` | Group divider |
1240
+
1241
+ ## Accessibility
1242
+
1243
+ `Menu.Content` uses `role="menu"`, actions use `role="menuitem"`, checkbox items use `role="menuitemcheckbox"`, and radio items use `role="menuitemradio"`. Selectable items expose `aria-checked`; disabled items expose disabled state. In an overlay, the trigger exposes the open state and labels the menu content.
1244
+
1245
+ `Menu.GroupHeader`, `Menu.SectionLabel`, and `Menu.Separator` are not menu items and are skipped by arrow keys. A button in `Menu.ItemAction` on the header is a separate tab stop. `Menu.ItemControl` and `Menu.ItemIndicator` are presentational and hidden from the accessibility tree; the row owns the checked state. `Menu.ItemShortcut` is hidden as well.
1246
+
1247
+ On web, Arrow Up and Arrow Down move through enabled items, Home and End move to the first and last items, Enter or Space activates the focused item, Escape closes and restores trigger focus, and Tab closes before continuing from the trigger’s document position.