@commercetools/nimbus-mcp 3.2.0 → 3.4.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 (223) hide show
  1. package/README.md +9 -1
  2. package/data/docs/route-manifest.json +1608 -143
  3. package/data/docs/routes/components-buttons-button.json +25 -2
  4. package/data/docs/routes/components-chat-chat-message-list.json +448 -0
  5. package/data/docs/routes/components-chat-chat-message.json +580 -0
  6. package/data/docs/routes/components-content-markdown.json +548 -0
  7. package/data/docs/routes/components-data-display-item-group.json +413 -0
  8. package/data/docs/routes/components-data-display-item.json +611 -0
  9. package/data/docs/routes/components-feedback-activity-indicator.json +396 -0
  10. package/data/docs/routes/components-feedback-skeleton.json +481 -0
  11. package/data/docs/routes/components-feedback-skeletoncircle.json +429 -0
  12. package/data/docs/routes/components-feedback-skeletontext.json +417 -0
  13. package/data/docs/routes/components-feedback-tooltip.json +23 -1
  14. package/data/docs/routes/components-inputs-drop-zone.json +562 -0
  15. package/data/docs/routes/components-inputs-file-trigger.json +421 -0
  16. package/data/docs/routes/components-inputs-range-slider.json +633 -0
  17. package/data/docs/routes/components-inputs-slider.json +611 -0
  18. package/data/docs/routes/components-layout-splitter.json +3 -3
  19. package/data/docs/routes/components-media-avatar.json +16 -5
  20. package/data/docs/routes/components-navigation-breadcrumbs.json +565 -0
  21. package/data/docs/routes/components-navigation-menu.json +1 -1
  22. package/data/docs/routes/components-navigation-tabnav.json +80 -14
  23. package/data/docs/routes/components-navigation-tabs.json +44 -11
  24. package/data/docs/routes/components-navigation-tree.json +445 -0
  25. package/data/docs/routes/home-getting-started-mcp-server-overview.json +2 -2
  26. package/data/docs/routes/home-getting-started-mcp-server-setup.json +2 -2
  27. package/data/docs/routes/icons.json +3 -3
  28. package/data/docs/routes/patterns-buttons-floating-action-button.json +392 -0
  29. package/data/docs/routes/patterns-buttons.json +58 -0
  30. package/data/docs/search-index.json +1 -1
  31. package/data/docs/types/AccordionContent.json +24 -24
  32. package/data/docs/types/AccordionHeader.json +88 -88
  33. package/data/docs/types/AccordionItem.json +20 -20
  34. package/data/docs/types/AccordionRoot.json +14 -14
  35. package/data/docs/types/ActivityIndicator.json +202 -0
  36. package/data/docs/types/AlertDescription.json +8 -8
  37. package/data/docs/types/AlertDismissButton.json +98 -81
  38. package/data/docs/types/AlertTitle.json +8 -8
  39. package/data/docs/types/Avatar.json +19 -2
  40. package/data/docs/types/Badge.json +2 -2
  41. package/data/docs/types/Body.json +6 -6
  42. package/data/docs/types/Box.json +6 -6
  43. package/data/docs/types/Breadcrumbs.json +12 -0
  44. package/data/docs/types/BreadcrumbsItem.json +296 -0
  45. package/data/docs/types/BreadcrumbsItemProps.json +9 -0
  46. package/data/docs/types/BreadcrumbsProps.json +9 -0
  47. package/data/docs/types/BreadcrumbsRoot.json +110 -0
  48. package/data/docs/types/Button.json +106 -89
  49. package/data/docs/types/Calendar.json +56 -56
  50. package/data/docs/types/Caption.json +6 -6
  51. package/data/docs/types/CardRoot.json +2 -2
  52. package/data/docs/types/Cell.json +6 -6
  53. package/data/docs/types/ChatMessage.json +12 -0
  54. package/data/docs/types/ChatMessageActions.json +42 -0
  55. package/data/docs/types/ChatMessageAvatar.json +140 -0
  56. package/data/docs/types/ChatMessageBody.json +42 -0
  57. package/data/docs/types/ChatMessageList.json +12 -0
  58. package/data/docs/types/ChatMessageListItem.json +42 -0
  59. package/data/docs/types/ChatMessageListRoot.json +78 -0
  60. package/data/docs/types/ChatMessageMeta.json +42 -0
  61. package/data/docs/types/ChatMessageRoot.json +119 -0
  62. package/data/docs/types/ChatMessageTyping.json +42 -0
  63. package/data/docs/types/Checkbox.json +86 -86
  64. package/data/docs/types/Code.json +11 -11
  65. package/data/docs/types/Column.json +6 -6
  66. package/data/docs/types/ColumnGroup.json +6 -6
  67. package/data/docs/types/ColumnHeader.json +6 -6
  68. package/data/docs/types/ComboBoxListBox.json +70 -70
  69. package/data/docs/types/ComboBoxOption.json +64 -64
  70. package/data/docs/types/ComboBoxPopover.json +91 -70
  71. package/data/docs/types/ComboBoxRoot.json +8 -8
  72. package/data/docs/types/ComboBoxSection.json +24 -24
  73. package/data/docs/types/ComboBoxTrigger.json +6 -6
  74. package/data/docs/types/DataTable.json +2 -2
  75. package/data/docs/types/DataTableBody.json +20 -20
  76. package/data/docs/types/DataTableHeader.json +25 -25
  77. package/data/docs/types/DataTableRoot.json +2 -2
  78. package/data/docs/types/DataTableTable.json +6 -6
  79. package/data/docs/types/DateInput.json +73 -73
  80. package/data/docs/types/DatePicker.json +58 -58
  81. package/data/docs/types/DateRangePicker.json +86 -86
  82. package/data/docs/types/DateRangePickerField.json +86 -86
  83. package/data/docs/types/DefaultPageRoot.json +2 -2
  84. package/data/docs/types/DialogCloseTrigger.json +96 -79
  85. package/data/docs/types/DraggableListField.json +88 -60
  86. package/data/docs/types/DraggableListItem.json +52 -52
  87. package/data/docs/types/DraggableListRoot.json +88 -60
  88. package/data/docs/types/DrawerCloseTrigger.json +96 -79
  89. package/data/docs/types/DropZone.json +161 -0
  90. package/data/docs/types/FieldErrors.json +2 -2
  91. package/data/docs/types/FileTrigger.json +130 -0
  92. package/data/docs/types/Flex.json +22 -22
  93. package/data/docs/types/FloatingActionButton.json +1036 -0
  94. package/data/docs/types/Footer.json +6 -6
  95. package/data/docs/types/FormActionBar.json +1 -1
  96. package/data/docs/types/FormFieldRoot.json +2 -2
  97. package/data/docs/types/Grid.json +24 -24
  98. package/data/docs/types/Group.json +12 -12
  99. package/data/docs/types/Header.json +6 -6
  100. package/data/docs/types/Heading.json +8 -8
  101. package/data/docs/types/Icon.json +4 -4
  102. package/data/docs/types/IconButton.json +106 -89
  103. package/data/docs/types/IconToggleButton.json +71 -71
  104. package/data/docs/types/Image.json +43 -43
  105. package/data/docs/types/Indicator.json +6 -6
  106. package/data/docs/types/InlineSvg.json +2 -2
  107. package/data/docs/types/Item.json +6 -6
  108. package/data/docs/types/ItemActions.json +27 -0
  109. package/data/docs/types/ItemContent.json +27 -0
  110. package/data/docs/types/ItemDescription.json +27 -0
  111. package/data/docs/types/ItemFooter.json +27 -0
  112. package/data/docs/types/ItemGroup.json +12 -0
  113. package/data/docs/types/ItemGroupRoot.json +27 -0
  114. package/data/docs/types/ItemGroupSeparator.json +27 -0
  115. package/data/docs/types/ItemHeader.json +27 -0
  116. package/data/docs/types/ItemMedia.json +56 -0
  117. package/data/docs/types/ItemRoot.json +239 -0
  118. package/data/docs/types/ItemTitle.json +27 -0
  119. package/data/docs/types/Kbd.json +8 -8
  120. package/data/docs/types/Link.json +14 -14
  121. package/data/docs/types/ListIndicator.json +6 -6
  122. package/data/docs/types/ListItem.json +6 -6
  123. package/data/docs/types/ListRoot.json +11 -11
  124. package/data/docs/types/LoadingSpinner.json +2 -2
  125. package/data/docs/types/MakeElementFocusable.json +18 -18
  126. package/data/docs/types/Markdown.json +183 -0
  127. package/data/docs/types/MenuItem.json +62 -62
  128. package/data/docs/types/MenuRoot.json +55 -55
  129. package/data/docs/types/MenuSection.json +30 -30
  130. package/data/docs/types/MenuSubmenuTrigger.json +4 -4
  131. package/data/docs/types/MenuTrigger.json +88 -88
  132. package/data/docs/types/MultilineTextInput.json +104 -104
  133. package/data/docs/types/MultilineTextInputField.json +102 -102
  134. package/data/docs/types/NumberInput.json +86 -86
  135. package/data/docs/types/NumberInputField.json +82 -82
  136. package/data/docs/types/PageContentColumn.json +6 -6
  137. package/data/docs/types/PageContentRoot.json +6 -6
  138. package/data/docs/types/PasswordInput.json +100 -100
  139. package/data/docs/types/PasswordInputField.json +100 -100
  140. package/data/docs/types/ProgressBar.json +13 -13
  141. package/data/docs/types/RadioInputOption.json +56 -56
  142. package/data/docs/types/RadioInputRoot.json +48 -48
  143. package/data/docs/types/RangeCalendar.json +58 -58
  144. package/data/docs/types/RangeSlider.json +337 -0
  145. package/data/docs/types/RangeSliderProps.json +9 -0
  146. package/data/docs/types/Region.json +6 -6
  147. package/data/docs/types/RegionTarget.json +6 -6
  148. package/data/docs/types/RichTextInput.json +2 -2
  149. package/data/docs/types/Root.json +11 -11
  150. package/data/docs/types/Row.json +6 -6
  151. package/data/docs/types/ScrollArea.json +2 -2
  152. package/data/docs/types/SearchInput.json +106 -106
  153. package/data/docs/types/SearchInputField.json +102 -102
  154. package/data/docs/types/SelectOption.json +54 -54
  155. package/data/docs/types/SelectOptionGroup.json +18 -18
  156. package/data/docs/types/SelectOptions.json +64 -64
  157. package/data/docs/types/SelectRoot.json +92 -92
  158. package/data/docs/types/Separator.json +4 -4
  159. package/data/docs/types/SimpleGrid.json +28 -28
  160. package/data/docs/types/Skeleton.json +129 -0
  161. package/data/docs/types/SkeletonCircle.json +103 -0
  162. package/data/docs/types/SkeletonCircleProps.json +9 -0
  163. package/data/docs/types/SkeletonProps.json +9 -0
  164. package/data/docs/types/SkeletonText.json +62 -0
  165. package/data/docs/types/SkeletonTextProps.json +9 -0
  166. package/data/docs/types/Slider.json +337 -0
  167. package/data/docs/types/SlidingIndicatorGeometry.json +9 -0
  168. package/data/docs/types/SlidingIndicatorRects.json +9 -0
  169. package/data/docs/types/SplitButton.json +16 -20
  170. package/data/docs/types/Stack.json +2 -2
  171. package/data/docs/types/StepsRoot.json +2 -2
  172. package/data/docs/types/Switch.json +34 -34
  173. package/data/docs/types/TabNavItem.json +5 -5
  174. package/data/docs/types/TabNavRoot.json +5 -5
  175. package/data/docs/types/TableBody.json +6 -6
  176. package/data/docs/types/TableCaption.json +6 -6
  177. package/data/docs/types/TableCell.json +6 -6
  178. package/data/docs/types/TableColumn.json +6 -6
  179. package/data/docs/types/TableColumnGroup.json +6 -6
  180. package/data/docs/types/TableColumnHeader.json +6 -6
  181. package/data/docs/types/TableFooter.json +6 -6
  182. package/data/docs/types/TableHeader.json +6 -6
  183. package/data/docs/types/TableRoot.json +19 -19
  184. package/data/docs/types/TableRow.json +6 -6
  185. package/data/docs/types/TableScrollArea.json +6 -6
  186. package/data/docs/types/TabsList.json +2 -2
  187. package/data/docs/types/TabsPanel.json +2 -2
  188. package/data/docs/types/TabsPanels.json +2 -2
  189. package/data/docs/types/TabsRoot.json +3 -3
  190. package/data/docs/types/TabsTab.json +2 -2
  191. package/data/docs/types/TagGroupRoot.json +24 -24
  192. package/data/docs/types/TagGroupTag.json +56 -56
  193. package/data/docs/types/TagGroupTagList.json +14 -14
  194. package/data/docs/types/Text.json +8 -8
  195. package/data/docs/types/TextInput.json +102 -102
  196. package/data/docs/types/TextInputField.json +100 -100
  197. package/data/docs/types/TimeInput.json +68 -68
  198. package/data/docs/types/ToggleButton.json +73 -73
  199. package/data/docs/types/ToggleButtonGroupButton.json +30 -30
  200. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  201. package/data/docs/types/Toolbar.json +9 -9
  202. package/data/docs/types/TooltipContent.json +28 -28
  203. package/data/docs/types/TooltipRoot.json +16 -16
  204. package/data/docs/types/Tree.json +12 -0
  205. package/data/docs/types/TreeIndicator.json +828 -0
  206. package/data/docs/types/TreeItem.json +559 -0
  207. package/data/docs/types/TreeItemContent.json +27 -0
  208. package/data/docs/types/TreeRoot.json +636 -0
  209. package/data/docs/types/TreeSubTree.json +40 -0
  210. package/data/docs/types/VisuallyHidden.json +6 -6
  211. package/data/docs/types/createItemsFromCsvDrop.json +4 -4
  212. package/data/docs/types/createItemsFromDirectoryDrop.json +4 -4
  213. package/data/docs/types/createItemsFromFileDrop.json +4 -4
  214. package/data/docs/types/createItemsFromImageDrop.json +4 -4
  215. package/data/docs/types/createItemsFromJsonDrop.json +4 -4
  216. package/data/docs/types/manifest.json +50 -0
  217. package/data/docs/types/useRegion.json +2 -2
  218. package/data/docs/types/useSlidingIndicator.json +145 -0
  219. package/data/docs/types/useTree.json +267 -0
  220. package/data/tokens.json +21 -0
  221. package/data/uikit-tokens.json +894 -0
  222. package/dist/index.js +3298 -109
  223. package/package.json +8 -6
@@ -123,7 +123,7 @@
123
123
  ]
124
124
  },
125
125
  "dev": {
126
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Menu, type MenuRootProps, type MenuItemProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation creates an action menu with items:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root onAction={(key) => alert(`Action: ${key}`)}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"duplicate\">Duplicate</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n## Usage examples\n\n### Custom triggers\n\nUse `asChild` to render custom trigger elements like Button or IconButton:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Menu.Root>\n <Menu.Trigger asChild>\n <Button variant=\"solid\" colorPalette=\"primary\">\n <Icons.KeyboardArrowDown />\n Button Trigger\n </Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"option1\">Option 1</Menu.Item>\n <Menu.Item id=\"option2\">Option 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root>\n <Menu.Trigger asChild>\n <IconButton\n variant=\"ghost\"\n colorPalette=\"neutral\"\n aria-label=\"More options\"\n >\n <Icons.MoreVert />\n </IconButton>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Stack>\n)\n```\n\n### Menu placement\n\nControl where the menu appears relative to the trigger with the `placement` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Menu.Root placement=\"bottom start\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Bottom Start</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root placement=\"bottom end\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Bottom End</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root placement=\"right\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Right</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Stack>\n)\n```\n\n### Rich item content\n\nMenu items support icons, labels, descriptions, and keyboard shortcuts using slots:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Edit Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"copy\">\n <Icon slot=\"icon\"><Icons.ContentCopy /></Icon>\n <Text slot=\"label\">Copy</Text>\n <Text slot=\"description\">Copy to clipboard</Text>\n <Kbd slot=\"keyboard\">⌘C</Kbd>\n </Menu.Item>\n <Menu.Item id=\"paste\">\n <Icon slot=\"icon\"><Icons.ContentPaste /></Icon>\n <Text slot=\"label\">Paste</Text>\n <Text slot=\"description\">Paste from clipboard</Text>\n <Kbd slot=\"keyboard\">⌘V</Kbd>\n </Menu.Item>\n <Menu.Item id=\"cut\">\n <Icon slot=\"icon\"><Icons.ContentCut /></Icon>\n <Text slot=\"label\">Cut</Text>\n <Kbd slot=\"keyboard\">⌘X</Kbd>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Critical items\n\nUse `isCritical` to highlight destructive actions:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>File Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"rename\">\n <Icon slot=\"icon\"><Icons.Edit /></Icon>\n <Text slot=\"label\">Rename</Text>\n </Menu.Item>\n <Menu.Item id=\"archive\">\n <Icon slot=\"icon\"><Icons.Archive /></Icon>\n <Text slot=\"label\">Archive</Text>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"delete\" isCritical>\n <Icon slot=\"icon\"><Icons.Delete /></Icon>\n <Text slot=\"label\">Delete</Text>\n <Text slot=\"description\">This action cannot be undone</Text>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Disabled items\n\nUse `isDisabled` on individual items to prevent interaction:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Edit</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"undo\">\n <Text slot=\"label\">Undo</Text>\n <Kbd slot=\"keyboard\">⌘Z</Kbd>\n </Menu.Item>\n <Menu.Item id=\"redo\" isDisabled>\n <Text slot=\"label\">Redo</Text>\n <Text slot=\"description\">Nothing to redo</Text>\n <Kbd slot=\"keyboard\">⌘⇧Z</Kbd>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"paste\" isDisabled>\n <Text slot=\"label\">Paste</Text>\n <Text slot=\"description\">Clipboard is empty</Text>\n <Kbd slot=\"keyboard\">⌘V</Kbd>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Sections with labels\n\nGroup related items using `Menu.Section` with a label:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Application Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"File\">\n <Menu.Item id=\"new\">New Document</Menu.Item>\n <Menu.Item id=\"open\">Open</Menu.Item>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Section>\n <Separator />\n <Menu.Section label=\"Edit\">\n <Menu.Item id=\"undo\">Undo</Menu.Item>\n <Menu.Item id=\"redo\">Redo</Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Single selection mode\n\nUse `selectionMode=\"single\"` for radio-button-like behavior within a section:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState<string>(\"medium\");\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root\n selectionMode=\"single\"\n selectedKeys={new Set([selected])}\n onSelectionChange={(keys) => {\n if (keys !== \"all\") {\n const newKey = Array.from(keys)[0] as string;\n setSelected(newKey);\n }\n }}\n >\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Text Size: {selected}</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"Choose Size\">\n <Menu.Item id=\"small\">\n <Text slot=\"label\">Small</Text>\n </Menu.Item>\n <Menu.Item id=\"medium\">\n <Text slot=\"label\">Medium</Text>\n </Menu.Item>\n <Menu.Item id=\"large\">\n <Text slot=\"label\">Large</Text>\n </Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">Selected: {selected}</Text>\n </Stack>\n );\n}\n```\n\n### Multiple selection mode\n\nUse `selectionMode=\"multiple\"` for checkbox-like behavior:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState<Set<string>>(new Set([\"bold\"]));\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root\n selectionMode=\"multiple\"\n selectedKeys={selected}\n onSelectionChange={(keys) => {\n if (keys !== \"all\") {\n setSelected(new Set(Array.from(keys) as string[]));\n }\n }}\n >\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Text Formatting</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"Style Options\">\n <Menu.Item id=\"bold\">\n <Text slot=\"label\">Bold</Text>\n <Kbd slot=\"keyboard\">⌘B</Kbd>\n </Menu.Item>\n <Menu.Item id=\"italic\">\n <Text slot=\"label\">Italic</Text>\n <Kbd slot=\"keyboard\">⌘I</Kbd>\n </Menu.Item>\n <Menu.Item id=\"underline\">\n <Text slot=\"label\">Underline</Text>\n <Kbd slot=\"keyboard\">⌘U</Kbd>\n </Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">\n Selected: {Array.from(selected).join(\", \") || \"None\"}\n </Text>\n </Stack>\n );\n}\n```\n\n### Submenus\n\nCreate nested menus using `Menu.SubmenuTrigger` and `Menu.Submenu`:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Settings</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"general\">\n <Icon slot=\"icon\"><Icons.Settings /></Icon>\n <Text slot=\"label\">General</Text>\n </Menu.Item>\n <Menu.SubmenuTrigger>\n <Menu.Item>\n <Icon slot=\"icon\"><Icons.AccountCircle /></Icon>\n <Text slot=\"label\">Account</Text>\n <Text slot=\"description\">Manage your account</Text>\n </Menu.Item>\n <Menu.Submenu>\n <Menu.Item id=\"profile\">Profile</Menu.Item>\n <Menu.Item id=\"security\">Security</Menu.Item>\n <Menu.Item id=\"notifications\">Notifications</Menu.Item>\n </Menu.Submenu>\n </Menu.SubmenuTrigger>\n <Menu.SubmenuTrigger>\n <Menu.Item>\n <Icon slot=\"icon\"><Icons.Palette /></Icon>\n <Text slot=\"label\">Appearance</Text>\n </Menu.Item>\n <Menu.Submenu>\n <Menu.Item id=\"theme\">Theme</Menu.Item>\n <Menu.Item id=\"language\">Language</Menu.Item>\n </Menu.Submenu>\n </Menu.SubmenuTrigger>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Uncontrolled mode\n\nFor simpler use cases, use `defaultOpen` and `onOpenChange`:\n\n```jsx live-dev\nconst App = () => {\n const [lastAction, setLastAction] = useState<string>(\"None\");\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Menu.Root\n onAction={(key) => setLastAction(key as string)}\n onOpenChange={(isOpen) => console.log(\"Menu open:\", isOpen)}\n placement=\"bottom end\"\n >\n <Menu.Trigger>Quick Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"new\">New File</Menu.Item>\n <Menu.Item id=\"open\">Open</Menu.Item>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">Last action: {lastAction}</Text>\n </Stack>\n );\n}\n```\n\n### Controlled mode\n\nFor programmatic control, use `isOpen` and `onOpenChange`:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState<boolean>(false);\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root isOpen={isOpen} onOpenChange={setIsOpen}>\n <Menu.Trigger>Controlled Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"item1\">Item 1</Menu.Item>\n <Menu.Item id=\"item2\">Item 2</Menu.Item>\n <Menu.Item id=\"item3\">Item 3</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n <Button onClick={() => setIsOpen(!isOpen)}>\n {isOpen ? \"Close\" : \"Open\"} Menu\n </Button>\n <Text fontSize=\"sm\">Menu is {isOpen ? \"open\" : \"closed\"}</Text>\n </Stack>\n );\n}\n```\n\n## Component requirements\n\n## Accessibility\n\nThe Menu handles most accessibility requirements internally through React Aria. However, you must ensure proper labeling:\n\n- **For default triggers**: The trigger text serves as the accessible name\n- **For icon-only triggers**: Always provide `aria-label`:\n\n```tsx\n<Menu.Trigger asChild>\n <IconButton aria-label=\"More options\">\n <Icons.MoreVert />\n </IconButton>\n</Menu.Trigger>\n```\n\n- **For custom triggers**: Ensure the trigger element has an accessible name\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-menu\";\n\nexport const Example = () => (\n <Menu.Root id={PERSISTENT_ID}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Navigate to/from the menu trigger\n- `Enter` / `Space`: Open the menu when trigger is focused\n- `Arrow Down` / `Arrow Up`: Navigate through menu items\n- `Arrow Right`: Open submenu (when focused on submenu trigger)\n- `Arrow Left`: Close submenu and return to parent menu\n- `Enter`: Select the focused item\n- `Space`: Toggle selection in selection modes\n- `Escape`: Close the menu\n- `Home` / `End`: Jump to first/last item\n- Type to search: Type characters to jump to matching items\n\n## API reference\n\n<PropsTable id=\"Menu\" />\n\n## Common patterns\n\n### Context menu for list items\n\nA common pattern for providing actions on list items:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { id: \"1\", name: \"Document A.pdf\" },\n { id: \"2\", name: \"Spreadsheet B.xlsx\" },\n { id: \"3\", name: \"Presentation C.pptx\" },\n ];\n\n const handleAction = (itemId, action) => {\n alert(`${action} on ${itemId}`);\n };\n\n return (\n <Stack direction=\"column\" gap=\"200\">\n {items.map((item) => (\n <Box\n key={item.id}\n display=\"flex\"\n justifyContent=\"space-between\"\n alignItems=\"center\"\n padding=\"300\"\n borderRadius=\"200\"\n bg=\"neutral.2\"\n >\n <Text>{item.name}</Text>\n <Menu.Root onAction={(action) => handleAction(item.id, action)}>\n <Menu.Trigger asChild>\n <IconButton\n variant=\"ghost\"\n aria-label={`Actions for ${item.name}`}\n >\n <Icons.MoreVert />\n </IconButton>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"open\">\n <Icon slot=\"icon\"><Icons.FolderOpen /></Icon>\n <Text slot=\"label\">Open</Text>\n </Menu.Item>\n <Menu.Item id=\"rename\">\n <Icon slot=\"icon\"><Icons.Edit /></Icon>\n <Text slot=\"label\">Rename</Text>\n </Menu.Item>\n <Menu.Item id=\"download\">\n <Icon slot=\"icon\"><Icons.Download /></Icon>\n <Text slot=\"label\">Download</Text>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"delete\" isCritical>\n <Icon slot=\"icon\"><Icons.Delete /></Icon>\n <Text slot=\"label\">Delete</Text>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Box>\n ))}\n </Stack>\n );\n}\n```\n\n### Dynamic menu items\n\nBuild menus from data arrays:\n\n```jsx live-dev\nconst App = () => {\n const menuItems = [\n { id: \"new\", label: \"New File\", icon: Icons.InsertDriveFile, kbd: \"⌘N\" },\n { id: \"open\", label: \"Open\", icon: Icons.FolderOpen, kbd: \"⌘O\" },\n { id: \"save\", label: \"Save\", icon: Icons.Save, kbd: \"⌘S\" },\n { id: \"divider\" },\n { id: \"delete\", label: \"Delete\", icon: Icons.Delete, critical: true },\n ];\n\n return (\n <Menu.Root onAction={(key) => alert(`Action: ${key}`)}>\n <Menu.Trigger asChild>\n <Button variant=\"outline\">\n <Icons.MoreVert />\n File Menu\n </Button>\n </Menu.Trigger>\n <Menu.Content>\n {menuItems.map((item) => {\n if (item.id === \"divider\") {\n return <Separator key={Math.random()} />;\n }\n return (\n <Menu.Item key={item.id} id={item.id} isCritical={item.critical}>\n {item.icon && (\n <Icon slot=\"icon\"><item.icon /></Icon>\n )}\n <Text slot=\"label\">{item.label}</Text>\n {item.kbd && <Kbd slot=\"keyboard\">{item.kbd}</Kbd>}\n </Menu.Item>\n );\n })}\n </Menu.Content>\n </Menu.Root>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Menu within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the menu renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Basic rendering\", () => {\n it(\"renders menu trigger\", () => {\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Actions\" })).toBeInTheDocument();\n });\n\n it(\"renders menu items when opened\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n expect(screen.getByRole(\"menuitem\", { name: \"Edit\" })).toBeInTheDocument();\n expect(\n screen.getByRole(\"menuitem\", { name: \"Delete\" })\n ).toBeInTheDocument();\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the menu\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Interactions\", () => {\n it(\"opens menu on trigger click\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n });\n\n it(\"closes menu when item is selected\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitem\", { name: \"Edit\" }));\n\n expect(handleAction).toHaveBeenCalledWith(\"edit\");\n await waitFor(() => {\n expect(screen.queryByRole(\"menu\")).not.toBeInTheDocument();\n });\n });\n\n it(\"closes menu on Escape key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.keyboard(\"{Escape}\");\n\n await waitFor(() => {\n expect(screen.queryByRole(\"menu\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Action Callback Tests\n\nTest the onAction callback behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Action callback\", () => {\n it(\"calls onAction with item id when clicked\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"copy\">Copy</Menu.Item>\n <Menu.Item id=\"paste\">Paste</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitem\", { name: \"Copy\" }));\n\n expect(handleAction).toHaveBeenCalledWith(\"copy\");\n });\n\n it(\"calls onAction with Enter key\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // First item should have focus\n await waitFor(() => {\n expect(screen.getByRole(\"menuitem\", { name: \"Save\" })).toHaveFocus();\n });\n\n await user.keyboard(\"{Enter}\");\n\n expect(handleAction).toHaveBeenCalledWith(\"save\");\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled open/close behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Controlled mode\", () => {\n it(\"opens when isOpen is true\", async () => {\n render(\n <NimbusProvider>\n <Menu.Root isOpen={true} onOpenChange={() => {}}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n });\n\n it(\"calls onOpenChange when menu is opened\", async () => {\n const user = userEvent.setup();\n const handleOpenChange = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root isOpen={false} onOpenChange={handleOpenChange}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n expect(handleOpenChange).toHaveBeenCalledWith(true);\n });\n});\n```\n\n### Disabled Items Tests\n\nTest disabled item behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Disabled items\", () => {\n it(\"renders disabled item with aria-disabled\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"paste\" isDisabled>\n Paste\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n const disabledItem = screen.getByRole(\"menuitem\", { name: \"Paste\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n });\n\n it(\"does not call onAction for disabled items\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"paste\" isDisabled>\n Paste\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Disabled items have pointer-events: none, so click won't fire\n // We verify the item is disabled\n const disabledItem = screen.getByRole(\"menuitem\", { name: \"Paste\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n\n // onAction should not have been called\n expect(handleAction).not.toHaveBeenCalled();\n });\n});\n```\n\n### Selection Mode Tests\n\nTest single and multiple selection modes\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Selection mode\", () => {\n it(\"renders radio items in single selection mode\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root selectionMode=\"single\" selectedKeys={new Set([\"medium\"])}>\n <Menu.Trigger>Size</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"small\">Small</Menu.Item>\n <Menu.Item id=\"medium\">Medium</Menu.Item>\n <Menu.Item id=\"large\">Large</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Size\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Items should be radio items in single selection mode\n expect(\n screen.getByRole(\"menuitemradio\", { name: \"Medium\" })\n ).toHaveAttribute(\"data-selected\");\n });\n\n it(\"renders checkbox items in multiple selection mode\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root\n selectionMode=\"multiple\"\n selectedKeys={new Set([\"bold\", \"italic\"])}\n >\n <Menu.Trigger>Formatting</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"bold\">Bold</Menu.Item>\n <Menu.Item id=\"italic\">Italic</Menu.Item>\n <Menu.Item id=\"underline\">Underline</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Formatting\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Items should be checkbox items in multiple selection mode\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Bold\" })\n ).toHaveAttribute(\"data-selected\");\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Italic\" })\n ).toHaveAttribute(\"data-selected\");\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Underline\" })\n ).not.toHaveAttribute(\"data-selected\");\n });\n\n it(\"calls onSelectionChange when selection changes\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root\n selectionMode=\"single\"\n selectedKeys={new Set([\"small\"])}\n onSelectionChange={handleSelectionChange}\n >\n <Menu.Trigger>Size</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"small\">Small</Menu.Item>\n <Menu.Item id=\"medium\">Medium</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Size\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitemradio\", { name: \"Medium\" }));\n\n expect(handleSelectionChange).toHaveBeenCalled();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-menu--docs)\n- [React Aria Menu](https://react-spectrum.adobe.com/react-aria/Menu.html)\n- [ARIA Menu Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/)\n",
126
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Menu, type MenuRootProps, type MenuItemProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation creates an action menu with items:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root onAction={(key) => alert(`Action: ${key}`)}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"duplicate\">Duplicate</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n## Usage examples\n\n### Custom triggers\n\nUse `asChild` to render custom trigger elements like Button or IconButton:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Menu.Root>\n <Menu.Trigger asChild>\n <Button variant=\"solid\" colorPalette=\"primary\">\n <Icons.KeyboardArrowDown />\n Button Trigger\n </Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"option1\">Option 1</Menu.Item>\n <Menu.Item id=\"option2\">Option 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root>\n <Menu.Trigger asChild>\n <IconButton\n variant=\"ghost\"\n colorPalette=\"neutral\"\n aria-label=\"More options\"\n >\n <Icons.MoreVert />\n </IconButton>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Stack>\n)\n```\n\n### Menu placement\n\nControl where the menu appears relative to the trigger with the `placement` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Menu.Root placement=\"bottom start\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Bottom Start</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root placement=\"bottom end\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Bottom End</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n\n <Menu.Root placement=\"right\">\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Right</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"1\">Item 1</Menu.Item>\n <Menu.Item id=\"2\">Item 2</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Stack>\n)\n```\n\n### Rich item content\n\nMenu items support icons, labels, descriptions, and keyboard shortcuts using slots:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Edit Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"copy\">\n <Icon slot=\"icon\"><Icons.ContentCopy /></Icon>\n <Text slot=\"label\">Copy</Text>\n <Text slot=\"description\">Copy to clipboard</Text>\n <Kbd slot=\"keyboard\">⌘C</Kbd>\n </Menu.Item>\n <Menu.Item id=\"paste\">\n <Icon slot=\"icon\"><Icons.ContentPaste /></Icon>\n <Text slot=\"label\">Paste</Text>\n <Text slot=\"description\">Paste from clipboard</Text>\n <Kbd slot=\"keyboard\">⌘V</Kbd>\n </Menu.Item>\n <Menu.Item id=\"cut\">\n <Icon slot=\"icon\"><Icons.ContentCut /></Icon>\n <Text slot=\"label\">Cut</Text>\n <Kbd slot=\"keyboard\">⌘X</Kbd>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Critical items\n\nUse `isCritical` to highlight destructive actions:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>File Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"rename\">\n <Icon slot=\"icon\"><Icons.Edit /></Icon>\n <Text slot=\"label\">Rename</Text>\n </Menu.Item>\n <Menu.Item id=\"archive\">\n <Icon slot=\"icon\"><Icons.Archive /></Icon>\n <Text slot=\"label\">Archive</Text>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"delete\" isCritical>\n <Icon slot=\"icon\"><Icons.Delete /></Icon>\n <Text slot=\"label\">Delete</Text>\n <Text slot=\"description\">This action cannot be undone</Text>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Disabled items\n\nUse `isDisabled` on individual items to prevent interaction:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Edit</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"undo\">\n <Text slot=\"label\">Undo</Text>\n <Kbd slot=\"keyboard\">⌘Z</Kbd>\n </Menu.Item>\n <Menu.Item id=\"redo\" isDisabled>\n <Text slot=\"label\">Redo</Text>\n <Text slot=\"description\">Nothing to redo</Text>\n <Kbd slot=\"keyboard\">⌘⇧Z</Kbd>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"paste\" isDisabled>\n <Text slot=\"label\">Paste</Text>\n <Text slot=\"description\">Clipboard is empty</Text>\n <Kbd slot=\"keyboard\">⌘V</Kbd>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Sections with labels\n\nGroup related items using `Menu.Section` with a label:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Application Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"File\">\n <Menu.Item id=\"new\">New Document</Menu.Item>\n <Menu.Item id=\"open\">Open</Menu.Item>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Section>\n <Separator />\n <Menu.Section label=\"Edit\">\n <Menu.Item id=\"undo\">Undo</Menu.Item>\n <Menu.Item id=\"redo\">Redo</Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Single selection mode\n\nUse `selectionMode=\"single\"` for radio-button-like behavior within a section:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState<string>(\"medium\");\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root\n selectionMode=\"single\"\n selectedKeys={new Set([selected])}\n onSelectionChange={(keys) => {\n if (keys !== \"all\") {\n const newKey = Array.from(keys)[0] as string;\n setSelected(newKey);\n }\n }}\n >\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Text Size: {selected}</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"Choose Size\">\n <Menu.Item id=\"small\">\n <Text slot=\"label\">Small</Text>\n </Menu.Item>\n <Menu.Item id=\"medium\">\n <Text slot=\"label\">Medium</Text>\n </Menu.Item>\n <Menu.Item id=\"large\">\n <Text slot=\"label\">Large</Text>\n </Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">Selected: {selected}</Text>\n </Stack>\n );\n}\n```\n\n### Multiple selection mode\n\nUse `selectionMode=\"multiple\"` for checkbox-like behavior:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState<Set<string>>(new Set([\"bold\"]));\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root\n selectionMode=\"multiple\"\n selectedKeys={selected}\n onSelectionChange={(keys) => {\n if (keys !== \"all\") {\n setSelected(new Set(Array.from(keys) as string[]));\n }\n }}\n >\n <Menu.Trigger asChild>\n <Button variant=\"outline\">Text Formatting</Button>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Section label=\"Style Options\">\n <Menu.Item id=\"bold\">\n <Text slot=\"label\">Bold</Text>\n <Kbd slot=\"keyboard\">⌘B</Kbd>\n </Menu.Item>\n <Menu.Item id=\"italic\">\n <Text slot=\"label\">Italic</Text>\n <Kbd slot=\"keyboard\">⌘I</Kbd>\n </Menu.Item>\n <Menu.Item id=\"underline\">\n <Text slot=\"label\">Underline</Text>\n <Kbd slot=\"keyboard\">⌘U</Kbd>\n </Menu.Item>\n </Menu.Section>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">\n Selected: {Array.from(selected).join(\", \") || \"None\"}\n </Text>\n </Stack>\n );\n}\n```\n\n### Submenus\n\nCreate nested menus using `Menu.SubmenuTrigger` and `Menu.Submenu`:\n\n```jsx live-dev\nconst App = () => (\n <Menu.Root>\n <Menu.Trigger>Settings</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"general\">\n <Icon slot=\"icon\"><Icons.Settings /></Icon>\n <Text slot=\"label\">General</Text>\n </Menu.Item>\n <Menu.SubmenuTrigger>\n <Menu.Item>\n <Icon slot=\"icon\"><Icons.AccountCircle /></Icon>\n <Text slot=\"label\">Account</Text>\n <Text slot=\"description\">Manage your account</Text>\n </Menu.Item>\n <Menu.Submenu>\n <Menu.Item id=\"profile\">Profile</Menu.Item>\n <Menu.Item id=\"security\">Security</Menu.Item>\n <Menu.Item id=\"notifications\">Notifications</Menu.Item>\n </Menu.Submenu>\n </Menu.SubmenuTrigger>\n <Menu.SubmenuTrigger>\n <Menu.Item>\n <Icon slot=\"icon\"><Icons.Palette /></Icon>\n <Text slot=\"label\">Appearance</Text>\n </Menu.Item>\n <Menu.Submenu>\n <Menu.Item id=\"theme\">Theme</Menu.Item>\n <Menu.Item id=\"language\">Language</Menu.Item>\n </Menu.Submenu>\n </Menu.SubmenuTrigger>\n </Menu.Content>\n </Menu.Root>\n)\n```\n\n### Uncontrolled mode\n\nFor simpler use cases, use `defaultOpen` and `onOpenChange`:\n\n```jsx live-dev\nconst App = () => {\n const [lastAction, setLastAction] = useState<string>(\"None\");\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Menu.Root\n onAction={(key) => setLastAction(key as string)}\n onOpenChange={(isOpen) => console.log(\"Menu open:\", isOpen)}\n placement=\"bottom end\"\n >\n <Menu.Trigger>Quick Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"new\">New File</Menu.Item>\n <Menu.Item id=\"open\">Open</Menu.Item>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n <Text fontSize=\"sm\">Last action: {lastAction}</Text>\n </Stack>\n );\n}\n```\n\n### Controlled mode\n\nFor programmatic control, use `isOpen` and `onOpenChange`:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState<boolean>(false);\n\n return (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Menu.Root isOpen={isOpen} onOpenChange={setIsOpen}>\n <Menu.Trigger>Controlled Menu</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"item1\">Item 1</Menu.Item>\n <Menu.Item id=\"item2\">Item 2</Menu.Item>\n <Menu.Item id=\"item3\">Item 3</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n <Button onClick={() => setIsOpen(!isOpen)}>\n {isOpen ? \"Close\" : \"Open\"} Menu\n </Button>\n <Text fontSize=\"sm\">Menu is {isOpen ? \"open\" : \"closed\"}</Text>\n </Stack>\n );\n}\n```\n\n## Component requirements\n\n## Accessibility\n\nThe Menu handles most accessibility requirements internally through React Aria. However, you must ensure proper labeling:\n\n- **For default triggers**: The trigger text serves as the accessible name\n- **For icon-only triggers**: Always provide `aria-label`:\n\n```tsx\n<Menu.Trigger asChild>\n <IconButton aria-label=\"More options\">\n <Icons.MoreVert />\n </IconButton>\n</Menu.Trigger>\n```\n\n- **For custom triggers**: Ensure the trigger element has an accessible name\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-menu\";\n\nexport const Example = () => (\n <Menu.Root id={PERSISTENT_ID}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Navigate to/from the menu trigger\n- `Enter` / `Space`: Open the menu when trigger is focused\n- `Arrow Down` / `Arrow Up`: Navigate through menu items\n- `Arrow Right`: Open submenu (when focused on submenu trigger)\n- `Arrow Left`: Close submenu and return to parent menu\n- `Enter`: Select the focused item\n- `Space`: Toggle selection in selection modes\n- `Escape`: Close the menu\n- `Home` / `End`: Jump to first/last item\n- Type to search: Type characters to jump to matching items\n\n## API reference\n\n<PropsTable id=\"Menu\" />\n\n## Common patterns\n\n### Context menu for list items\n\nA common pattern for providing actions on list items:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { id: \"1\", name: \"Document A.pdf\" },\n { id: \"2\", name: \"Spreadsheet B.xlsx\" },\n { id: \"3\", name: \"Presentation C.pptx\" },\n ];\n\n const handleAction = (itemId, action) => {\n alert(`${action} on ${itemId}`);\n };\n\n return (\n <Stack direction=\"column\" gap=\"200\">\n {items.map((item) => (\n <Box\n key={item.id}\n display=\"flex\"\n justifyContent=\"space-between\"\n alignItems=\"center\"\n padding=\"300\"\n borderRadius=\"200\"\n bg=\"neutral.2\"\n >\n <Text>{item.name}</Text>\n <Menu.Root onAction={(action) => handleAction(item.id, action)}>\n <Menu.Trigger asChild>\n <IconButton\n variant=\"ghost\"\n aria-label={`Actions for ${item.name}`}\n >\n <Icons.MoreVert />\n </IconButton>\n </Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"open\">\n <Icon slot=\"icon\"><Icons.FolderOpen /></Icon>\n <Text slot=\"label\">Open</Text>\n </Menu.Item>\n <Menu.Item id=\"rename\">\n <Icon slot=\"icon\"><Icons.Edit /></Icon>\n <Text slot=\"label\">Rename</Text>\n </Menu.Item>\n <Menu.Item id=\"download\">\n <Icon slot=\"icon\"><Icons.Download /></Icon>\n <Text slot=\"label\">Download</Text>\n </Menu.Item>\n <Separator />\n <Menu.Item id=\"delete\" isCritical>\n <Icon slot=\"icon\"><Icons.Delete /></Icon>\n <Text slot=\"label\">Delete</Text>\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </Box>\n ))}\n </Stack>\n );\n}\n```\n\n### Dynamic menu items\n\nBuild menus from data arrays:\n\n```jsx live-dev\nconst App = () => {\n const menuItems = [\n { id: \"new\", label: \"New File\", icon: Icons.InsertDriveFile, kbd: \"⌘N\" },\n { id: \"open\", label: \"Open\", icon: Icons.FolderOpen, kbd: \"⌘O\" },\n { id: \"save\", label: \"Save\", icon: Icons.Save, kbd: \"⌘S\" },\n { id: \"divider\" },\n { id: \"delete\", label: \"Delete\", icon: Icons.Delete, critical: true },\n ];\n\n return (\n <Menu.Root onAction={(key) => alert(`Action: ${key}`)}>\n <Menu.Trigger asChild>\n <Button variant=\"outline\">\n <Icons.MoreVert />\n File Menu\n </Button>\n </Menu.Trigger>\n <Menu.Content>\n {menuItems.map((item) => {\n if (item.id === \"divider\") {\n return <Separator key={Math.random()} />;\n }\n return (\n <Menu.Item key={item.id} id={item.id} isCritical={item.critical}>\n {item.icon && (\n <Icon slot=\"icon\"><item.icon /></Icon>\n )}\n <Text slot=\"label\">{item.label}</Text>\n {item.kbd && <Kbd slot=\"keyboard\">{item.kbd}</Kbd>}\n </Menu.Item>\n );\n })}\n </Menu.Content>\n </Menu.Root>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Menu within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the menu renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Basic rendering\", () => {\n it(\"renders menu trigger\", () => {\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Actions\" })).toBeInTheDocument();\n });\n\n it(\"renders menu items when opened\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"delete\">Delete</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n expect(screen.getByRole(\"menuitem\", { name: \"Edit\" })).toBeInTheDocument();\n expect(\n screen.getByRole(\"menuitem\", { name: \"Delete\" })\n ).toBeInTheDocument();\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the menu\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Interactions\", () => {\n it(\"opens menu on trigger click\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n });\n\n it(\"closes menu when item is selected\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitem\", { name: \"Edit\" }));\n\n expect(handleAction).toHaveBeenCalledWith(\"edit\", undefined);\n await waitFor(() => {\n expect(screen.queryByRole(\"menu\")).not.toBeInTheDocument();\n });\n });\n\n it(\"closes menu on Escape key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.keyboard(\"{Escape}\");\n\n await waitFor(() => {\n expect(screen.queryByRole(\"menu\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Action Callback Tests\n\nTest the onAction callback behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Action callback\", () => {\n it(\"calls onAction with item id when clicked\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"copy\">Copy</Menu.Item>\n <Menu.Item id=\"paste\">Paste</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitem\", { name: \"Copy\" }));\n\n expect(handleAction).toHaveBeenCalledWith(\"copy\", undefined);\n });\n\n it(\"calls onAction with Enter key\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"save\">Save</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // First item should have focus\n await waitFor(() => {\n expect(screen.getByRole(\"menuitem\", { name: \"Save\" })).toHaveFocus();\n });\n\n await user.keyboard(\"{Enter}\");\n\n expect(handleAction).toHaveBeenCalledWith(\"save\", undefined);\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled open/close behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Controlled mode\", () => {\n it(\"opens when isOpen is true\", async () => {\n render(\n <NimbusProvider>\n <Menu.Root isOpen={true} onOpenChange={() => {}}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n });\n\n it(\"calls onOpenChange when menu is opened\", async () => {\n const user = userEvent.setup();\n const handleOpenChange = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root isOpen={false} onOpenChange={handleOpenChange}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n expect(handleOpenChange).toHaveBeenCalledWith(true);\n });\n});\n```\n\n### Disabled Items Tests\n\nTest disabled item behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Disabled items\", () => {\n it(\"renders disabled item with aria-disabled\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"edit\">Edit</Menu.Item>\n <Menu.Item id=\"paste\" isDisabled>\n Paste\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n const disabledItem = screen.getByRole(\"menuitem\", { name: \"Paste\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n });\n\n it(\"does not call onAction for disabled items\", async () => {\n const user = userEvent.setup();\n const handleAction = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root onAction={handleAction}>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"paste\" isDisabled>\n Paste\n </Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Actions\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Disabled items have pointer-events: none, so click won't fire\n // We verify the item is disabled\n const disabledItem = screen.getByRole(\"menuitem\", { name: \"Paste\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n\n // onAction should not have been called\n expect(handleAction).not.toHaveBeenCalled();\n });\n});\n```\n\n### Selection Mode Tests\n\nTest single and multiple selection modes\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Menu, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Menu - Selection mode\", () => {\n it(\"renders radio items in single selection mode\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root selectionMode=\"single\" selectedKeys={new Set([\"medium\"])}>\n <Menu.Trigger>Size</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"small\">Small</Menu.Item>\n <Menu.Item id=\"medium\">Medium</Menu.Item>\n <Menu.Item id=\"large\">Large</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Size\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Items should be radio items in single selection mode\n expect(\n screen.getByRole(\"menuitemradio\", { name: \"Medium\" })\n ).toHaveAttribute(\"data-selected\");\n });\n\n it(\"renders checkbox items in multiple selection mode\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Menu.Root\n selectionMode=\"multiple\"\n selectedKeys={new Set([\"bold\", \"italic\"])}\n >\n <Menu.Trigger>Formatting</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"bold\">Bold</Menu.Item>\n <Menu.Item id=\"italic\">Italic</Menu.Item>\n <Menu.Item id=\"underline\">Underline</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Formatting\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n // Items should be checkbox items in multiple selection mode\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Bold\" })\n ).toHaveAttribute(\"data-selected\");\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Italic\" })\n ).toHaveAttribute(\"data-selected\");\n expect(\n screen.getByRole(\"menuitemcheckbox\", { name: \"Underline\" })\n ).not.toHaveAttribute(\"data-selected\");\n });\n\n it(\"calls onSelectionChange when selection changes\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <Menu.Root\n selectionMode=\"single\"\n selectedKeys={new Set([\"small\"])}\n onSelectionChange={handleSelectionChange}\n >\n <Menu.Trigger>Size</Menu.Trigger>\n <Menu.Content>\n <Menu.Item id=\"small\">Small</Menu.Item>\n <Menu.Item id=\"medium\">Medium</Menu.Item>\n </Menu.Content>\n </Menu.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Size\" }));\n\n await waitFor(() => {\n expect(screen.getByRole(\"menu\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"menuitemradio\", { name: \"Medium\" }));\n\n expect(handleSelectionChange).toHaveBeenCalled();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-menu--docs)\n- [React Aria Menu](https://react-spectrum.adobe.com/react-aria/Menu.html)\n- [ARIA Menu Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/)\n",
127
127
  "toc": [
128
128
  {
129
129
  "value": "Getting started",
@@ -61,6 +61,28 @@
61
61
  ],
62
62
  "parent": "root"
63
63
  },
64
+ {
65
+ "value": "Variants",
66
+ "href": "#variants",
67
+ "depth": 3,
68
+ "numbering": [
69
+ 1,
70
+ 2,
71
+ 2
72
+ ],
73
+ "parent": "root"
74
+ },
75
+ {
76
+ "value": "Animated highlight",
77
+ "href": "#animated-highlight",
78
+ "depth": 3,
79
+ "numbering": [
80
+ 1,
81
+ 2,
82
+ 3
83
+ ],
84
+ "parent": "root"
85
+ },
64
86
  {
65
87
  "value": "Active item",
66
88
  "href": "#active-item",
@@ -68,7 +90,7 @@
68
90
  "numbering": [
69
91
  1,
70
92
  2,
71
- 2
93
+ 4
72
94
  ],
73
95
  "parent": "root"
74
96
  },
@@ -79,7 +101,7 @@
79
101
  "numbering": [
80
102
  1,
81
103
  2,
82
- 3
104
+ 5
83
105
  ],
84
106
  "parent": "root"
85
107
  },
@@ -90,7 +112,7 @@
90
112
  "numbering": [
91
113
  1,
92
114
  2,
93
- 4
115
+ 6
94
116
  ],
95
117
  "parent": "root"
96
118
  }
@@ -114,10 +136,10 @@
114
136
  }
115
137
  ]
116
138
  },
117
- "mdx": "\n## Overview\n\nTabNav provides tab-styled navigation links for route-based page navigation.\nUnlike `Tabs`, which switch content panels within the same page, `TabNav` renders\na `<nav>` landmark containing `<a>` anchor elements that link to distinct routes.\nEach item behaves as a standard link, and the active item is identified with\n`aria-current=\"page\"`.\n\nUse `TabNav` in page headers where each item links to a different URL (e.g.,\n`/orders/123/general`, `/orders/123/items`).\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n\n## Variables\n\nGet familiar with the features.\n\n### Sizes\n\n`TabNav` supports three size variants — `sm`, `md` (default), and `lg` — that\ncontrol font size and padding on each item.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n {[\"sm\", \"md\", \"lg\"].map((size) => (\n <Stack key={size} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{size}</Text>\n <TabNav.Root aria-label={`Order navigation (${size})`} size={size}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This renders `aria-current=\"page\"`\non the anchor, which screen readers announce as \"current page\" and which applies\nthe active underline style.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### Disabled item\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible.\nDisabled items are visually dimmed and excluded from the tab sequence.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### With icons\n\nTab items support inline icons alongside text via the built-in flex gap layout.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n",
139
+ "mdx": "\n## Overview\n\nTabNav provides tab-styled navigation links for route-based page navigation.\nUnlike `Tabs`, which switch content panels within the same page, `TabNav` renders\na `<nav>` landmark containing `<a>` anchor elements that link to distinct routes.\nEach item behaves as a standard link, and the active item is identified with\n`aria-current=\"page\"`.\n\nUse `TabNav` in page headers where each item links to a different URL (e.g.,\n`/orders/123/general`, `/orders/123/items`).\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n\n## Variables\n\nGet familiar with the features.\n\n### Sizes\n\n`TabNav` supports three size variants — `sm`, `md` (default), and `lg` — that\ncontrol font size and padding on each item.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n {[\"sm\", \"md\", \"lg\"].map((size) => (\n <Stack key={size} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{size}</Text>\n <TabNav.Root aria-label={`Order navigation (${size})`} size={size}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Variants\n\n`TabNav` ships with three visual variants:\n\n- **`line`** (default) — an underline strip beneath the active item.\n- **`rounded`** — a soft rounded-rect highlight behind the active item.\n- **`pill`** — a fully-rounded capsule highlight behind the active item.\n\nThe `rounded` and `pill` variants drop the baseline and add a small gap\nbetween items. Their active highlight is themeable via `colorPalette` (it\ndefaults to `primary`).\n\nThe active highlight always slides smoothly between items as the active item\nchanges — a single sliding indicator rather than a static per-item highlight.\nThe motion automatically respects `prefers-reduced-motion: reduce`: when reduced\nmotion is requested, the highlight snaps into place instead of sliding.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"600\">\n {[\"line\", \"rounded\", \"pill\"].map((variant) => (\n <Stack key={variant} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{variant}</Text>\n <TabNav.Root aria-label={`Order navigation (${variant})`} variant={variant}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Animated highlight\n\nThe active highlight slides between items whenever the active item changes (an\nunderline bar for `line`, a filled highlight for `rounded`/`pill`) — no prop\nrequired. The slide respects `prefers-reduced-motion: reduce` (the highlight\nsnaps into place), and `aria-current`, focus, and keyboard order are unaffected.\n\n```jsx live\nconst App = () => {\n const items = [\n { href: \"/orders/123/general\", label: \"General\" },\n { href: \"/orders/123/items\", label: \"Items\" },\n { href: \"/orders/123/shipping\", label: \"Shipping\" },\n ];\n const [activePath, setActivePath] = React.useState(items[0].href);\n return (\n <TabNav.Root aria-label=\"Order navigation\" variant=\"rounded\">\n {items.map((item) => (\n <TabNav.Item\n key={item.href}\n href={item.href}\n isCurrent={activePath === item.href}\n onClick={(e) => {\n e.preventDefault();\n setActivePath(item.href);\n }}\n >\n {item.label}\n </TabNav.Item>\n ))}\n </TabNav.Root>\n );\n}\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This renders `aria-current=\"page\"`\non the anchor, which screen readers announce as \"current page\" and which applies\nthe active underline style.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### Disabled item\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible.\nDisabled items are visually dimmed and excluded from the tab sequence.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### With icons\n\nTab items support inline icons alongside text via the built-in flex gap layout.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n",
118
140
  "views": {
119
141
  "overview": {
120
- "mdx": "\n## Overview\n\nTabNav provides tab-styled navigation links for route-based page navigation.\nUnlike `Tabs`, which switch content panels within the same page, `TabNav` renders\na `<nav>` landmark containing `<a>` anchor elements that link to distinct routes.\nEach item behaves as a standard link, and the active item is identified with\n`aria-current=\"page\"`.\n\nUse `TabNav` in page headers where each item links to a different URL (e.g.,\n`/orders/123/general`, `/orders/123/items`).\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n\n## Variables\n\nGet familiar with the features.\n\n### Sizes\n\n`TabNav` supports three size variants — `sm`, `md` (default), and `lg` — that\ncontrol font size and padding on each item.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n {[\"sm\", \"md\", \"lg\"].map((size) => (\n <Stack key={size} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{size}</Text>\n <TabNav.Root aria-label={`Order navigation (${size})`} size={size}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This renders `aria-current=\"page\"`\non the anchor, which screen readers announce as \"current page\" and which applies\nthe active underline style.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### Disabled item\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible.\nDisabled items are visually dimmed and excluded from the tab sequence.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### With icons\n\nTab items support inline icons alongside text via the built-in flex gap layout.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n",
142
+ "mdx": "\n## Overview\n\nTabNav provides tab-styled navigation links for route-based page navigation.\nUnlike `Tabs`, which switch content panels within the same page, `TabNav` renders\na `<nav>` landmark containing `<a>` anchor elements that link to distinct routes.\nEach item behaves as a standard link, and the active item is identified with\n`aria-current=\"page\"`.\n\nUse `TabNav` in page headers where each item links to a different URL (e.g.,\n`/orders/123/general`, `/orders/123/items`).\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n\n## Variables\n\nGet familiar with the features.\n\n### Sizes\n\n`TabNav` supports three size variants — `sm`, `md` (default), and `lg` — that\ncontrol font size and padding on each item.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n {[\"sm\", \"md\", \"lg\"].map((size) => (\n <Stack key={size} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{size}</Text>\n <TabNav.Root aria-label={`Order navigation (${size})`} size={size}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Variants\n\n`TabNav` ships with three visual variants:\n\n- **`line`** (default) — an underline strip beneath the active item.\n- **`rounded`** — a soft rounded-rect highlight behind the active item.\n- **`pill`** — a fully-rounded capsule highlight behind the active item.\n\nThe `rounded` and `pill` variants drop the baseline and add a small gap\nbetween items. Their active highlight is themeable via `colorPalette` (it\ndefaults to `primary`).\n\nThe active highlight always slides smoothly between items as the active item\nchanges — a single sliding indicator rather than a static per-item highlight.\nThe motion automatically respects `prefers-reduced-motion: reduce`: when reduced\nmotion is requested, the highlight snaps into place instead of sliding.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"column\" gap=\"600\">\n {[\"line\", \"rounded\", \"pill\"].map((variant) => (\n <Stack key={variant} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"600\">{variant}</Text>\n <TabNav.Root aria-label={`Order navigation (${variant})`} variant={variant}>\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n)\n```\n\n### Animated highlight\n\nThe active highlight slides between items whenever the active item changes (an\nunderline bar for `line`, a filled highlight for `rounded`/`pill`) — no prop\nrequired. The slide respects `prefers-reduced-motion: reduce` (the highlight\nsnaps into place), and `aria-current`, focus, and keyboard order are unaffected.\n\n```jsx live\nconst App = () => {\n const items = [\n { href: \"/orders/123/general\", label: \"General\" },\n { href: \"/orders/123/items\", label: \"Items\" },\n { href: \"/orders/123/shipping\", label: \"Shipping\" },\n ];\n const [activePath, setActivePath] = React.useState(items[0].href);\n return (\n <TabNav.Root aria-label=\"Order navigation\" variant=\"rounded\">\n {items.map((item) => (\n <TabNav.Item\n key={item.href}\n href={item.href}\n isCurrent={activePath === item.href}\n onClick={(e) => {\n e.preventDefault();\n setActivePath(item.href);\n }}\n >\n {item.label}\n </TabNav.Item>\n ))}\n </TabNav.Root>\n );\n}\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This renders `aria-current=\"page\"`\non the anchor, which screen readers announce as \"current page\" and which applies\nthe active underline style.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### Disabled item\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible.\nDisabled items are visually dimmed and excluded from the tab sequence.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n\n### With icons\n\nTab items support inline icons alongside text via the built-in flex gap layout.\n\n```jsx live\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n)\n```\n",
121
143
  "toc": [
122
144
  {
123
145
  "value": "Overview",
@@ -161,6 +183,28 @@
161
183
  ],
162
184
  "parent": "root"
163
185
  },
186
+ {
187
+ "value": "Variants",
188
+ "href": "#variants",
189
+ "depth": 3,
190
+ "numbering": [
191
+ 1,
192
+ 2,
193
+ 2
194
+ ],
195
+ "parent": "root"
196
+ },
197
+ {
198
+ "value": "Animated highlight",
199
+ "href": "#animated-highlight",
200
+ "depth": 3,
201
+ "numbering": [
202
+ 1,
203
+ 2,
204
+ 3
205
+ ],
206
+ "parent": "root"
207
+ },
164
208
  {
165
209
  "value": "Active item",
166
210
  "href": "#active-item",
@@ -168,7 +212,7 @@
168
212
  "numbering": [
169
213
  1,
170
214
  2,
171
- 2
215
+ 4
172
216
  ],
173
217
  "parent": "root"
174
218
  },
@@ -179,7 +223,7 @@
179
223
  "numbering": [
180
224
  1,
181
225
  2,
182
- 3
226
+ 5
183
227
  ],
184
228
  "parent": "root"
185
229
  },
@@ -190,7 +234,7 @@
190
234
  "numbering": [
191
235
  1,
192
236
  2,
193
- 4
237
+ 6
194
238
  ],
195
239
  "parent": "root"
196
240
  }
@@ -223,7 +267,7 @@
223
267
  ]
224
268
  },
225
269
  "dev": {
226
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { TabNav, type TabNavProps, type TabNavItemProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\n`TabNav` is a compound component for **page-level route-based navigation** that renders a `<nav>` landmark containing `<a>` anchor elements. The active item is identified with `aria-current=\"page\"`.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## TabNav vs Tabs\n\n`TabNav` and `Tabs` look similar but serve fundamentally different purposes:\n\n| | `TabNav` | `Tabs` |\n|---|---|---|\n| **Use case** | Page-level navigation between routes | Switching content panels within a page |\n| **HTML semantics** | `<nav>` + `<a>` | `role=\"tablist\"` + `role=\"tab\"` |\n| **Active state** | `aria-current=\"page\"` | `aria-selected=\"true\"` |\n| **Keyboard** | Sequential Tab key | Arrow keys (roving tabindex) |\n| **Content** | No panel — content managed by router | Owns associated `TabPanel` |\n| **URL** | Always (`href` required) | Optional (`href` prop) |\n\nBoth `TabNav` and `Tabs` support client-side routing — the deciding factor is whether the component owns its content panels or delegates to the router. Use `TabNav` when each item navigates to a different route and the router manages what's rendered. Use `Tabs` when the content lives alongside the tab list.\n\n## Usage examples\n\n### Client-side view switching\n\nThe most common TabNav pattern: a persistent nav bar where clicking an item swaps the visible content without a full page reload. In a real app, `isCurrent` is derived from the router and the `href` triggers a route change. The example below simulates this with local state to show the interaction.\n\n```jsx live-dev\nconst items = [\n { href: '/orders/123/general', label: 'General' },\n { href: '/orders/123/items', label: 'Items' },\n { href: '/orders/123/shipping', label: 'Shipping' },\n];\n\nconst content = {\n '/orders/123/general': 'General settings and order details.',\n '/orders/123/items': 'Line items and quantities.',\n '/orders/123/shipping': 'Shipping address and carrier information.',\n};\n\nconst App = () => {\n const [activePath, setActivePath] = React.useState(items[0].href);\n\n return (\n <Stack gap=\"400\">\n <TabNav.Root aria-label=\"Order navigation\">\n {items.map((item) => (\n <TabNav.Item\n key={item.href}\n href={item.href}\n isCurrent={activePath === item.href}\n onClick={(e) => { e.preventDefault(); setActivePath(item.href); }}\n >\n {item.label}\n </TabNav.Item>\n ))}\n </TabNav.Root>\n <Box padding=\"400\">{content[activePath]}</Box>\n </Stack>\n );\n};\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This sets `aria-current=\"page\"` on the anchor element, which screen readers announce as \"current page\".\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n);\n```\n\nWhen using a client-side router, set `isCurrent` based on the active route:\n\n```tsx\nimport { useLocation } from 'react-router-dom';\n\nconst OrderNav = ({ orderId }: { orderId: string }) => {\n const { pathname } = useLocation();\n\n return (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item\n href={`/orders/${orderId}/general`}\n isCurrent={pathname.endsWith('/general')}\n >\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/items`}\n isCurrent={pathname.endsWith('/items')}\n >\n Items\n </TabNav.Item>\n </TabNav.Root>\n );\n};\n```\n\n### Size variants\n\nThe `size` prop controls font size and padding. Available sizes: `sm`, `md` (default), `lg`.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Small</Text>\n <TabNav.Root aria-label=\"Navigation (sm)\" size=\"sm\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Medium (default)</Text>\n <TabNav.Root aria-label=\"Navigation (md)\" size=\"md\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Large</Text>\n <TabNav.Root aria-label=\"Navigation (lg)\" size=\"lg\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n </Stack>\n);\n```\n\n### Disabled items\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible. Disabled items are visually dimmed, removed from the tab sequence, and their `href` is stripped.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### External links\n\nUse `target` and `rel` to open a navigation item in a new browser tab. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"` for security.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item\n href=\"https://docs.example.com/orders\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Docs ↗\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### With icons\n\nTab items support inline icons alongside text via the `gap` built into the item layout.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## Component requirements\n\n### Accessibility\n\nThe TabNav component uses semantic HTML for navigation — it does not use ARIA roles for the tablist pattern. Accessibility is handled natively by the browser's `<nav>` landmark and `<a>` anchor semantics. `TabNav.Item` uses `useLink` from React Aria for consistent interaction handling.\n\n#### Role\n\n- `TabNav.Root` renders a `<nav>` HTML element — a navigation landmark, **not** `role=\"tablist\"`\n- `TabNav.Item` renders an `<a>` HTML element with the implicit `link` role — **not** `role=\"tab\"`\n- The active item uses `aria-current=\"page\"` — **not** `aria-selected`\n\n#### Labeling\n\nAlways provide an `aria-label` on `TabNav.Root` to describe the navigation region. This is especially important when a page contains multiple `<nav>` landmarks.\n\n```tsx\n<TabNav.Root aria-label=\"Order navigation\">\n ...\n</TabNav.Root>\n```\n\n#### Keyboard navigation\n\nTabNav uses standard sequential keyboard navigation — no roving tabindex, no arrow key cycling.\n\n| Key | Action |\n|-----|--------|\n| `Tab` | Move focus to the next link |\n| `Shift + Tab` | Move focus to the previous link |\n| `Enter` | Activate the focused link |\n\nArrow keys (`ArrowRight`, `ArrowLeft`) do **not** move focus between items. This is intentional — TabNav is a `<nav>` with standard anchor links, not a `role=\"tablist\"` widget.\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id:\n\n```tsx\nconst PERSISTENT_ID = \"order-detail-tab-nav\";\n\nexport const OrderTabNav = ({ orderId }: { orderId: string }) => (\n <TabNav.Root id={PERSISTENT_ID} aria-label=\"Order navigation\">\n <TabNav.Item href={`/orders/${orderId}/general`} isCurrent>\n General\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## API reference\n\n<PropsTable id=\"TabNav\" />\n\n## Common patterns\n\n### Syncing active item with router\n\nIn most routing setups you derive `isCurrent` from the current URL rather than managing it in state.\n\n```tsx\n// React Router v6\nimport { useMatch } from 'react-router-dom';\n\nconst OrderNav = ({ orderId }: { orderId: string }) => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item\n href={`/orders/${orderId}/general`}\n isCurrent={!!useMatch(`/orders/${orderId}/general`)}\n >\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/items`}\n isCurrent={!!useMatch(`/orders/${orderId}/items`)}\n >\n Items\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/shipping`}\n isCurrent={!!useMatch(`/orders/${orderId}/shipping`)}\n >\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### Conditionally disabling items\n\nDisable items based on permissions or loading state rather than hiding them — this preserves the layout and communicates to users that the section exists.\n\n```tsx\nconst OrderNav = ({ orderId, canViewShipping }: { orderId: string; canViewShipping: boolean }) => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href={`/orders/${orderId}/general`} isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/shipping`}\n isDisabled={!canViewShipping}\n >\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using TabNav within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the TabNav component renders a nav landmark with anchor links\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Basic rendering\", () => {\n it(\"renders a navigation landmark with links\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"navigation\", { name: \"Order navigation\" })\n ).toBeInTheDocument();\n expect(screen.getAllByRole(\"link\")).toHaveLength(3);\n });\n\n it(\"renders items as anchor elements with correct href\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveAttribute(\n \"href\",\n \"/orders/123/general\"\n );\n expect(screen.getByRole(\"link\", { name: \"Items\" })).toHaveAttribute(\n \"href\",\n \"/orders/123/items\"\n );\n });\n});\n```\n\n### Active Item Tests\n\nVerify aria-current=\"page\" is applied correctly to the active navigation item\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Active item\", () => {\n it(\"sets aria-current='page' on the active item\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveAttribute(\n \"aria-current\",\n \"page\"\n );\n });\n\n it(\"does not set aria-current on inactive items\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"Items\" })).not.toHaveAttribute(\n \"aria-current\"\n );\n expect(screen.getByRole(\"link\", { name: \"Shipping\" })).not.toHaveAttribute(\n \"aria-current\"\n );\n });\n\n it(\"does not use aria-selected (navigation, not widget)\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).not.toHaveAttribute(\n \"aria-selected\"\n );\n });\n});\n```\n\n### Disabled Item Tests\n\nVerify disabled items are marked inaccessible and removed from tab order\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Disabled items\", () => {\n it(\"marks disabled items with aria-disabled='true'\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n const disabledItem = screen.getByRole(\"link\", { name: \"Shipping\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n });\n\n it(\"disabled items have no tabIndex (excluded from tab sequence by browser)\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n // React Aria sets no tabIndex on the disabled anchor — the browser excludes it\n const disabledItem = screen.getByRole(\"link\", { name: \"Shipping\" });\n expect(disabledItem).not.toHaveAttribute(\"tabindex\");\n });\n});\n```\n\n### External Link Tests\n\nVerify target and rel attributes for external navigation items\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - External links\", () => {\n it(\"applies target and rel to external link items\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item\n href=\"https://docs.example.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Docs ↗\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n const externalLink = screen.getByRole(\"link\", { name: \"Docs ↗\" });\n expect(externalLink).toHaveAttribute(\"target\", \"_blank\");\n expect(externalLink).toHaveAttribute(\"rel\", \"noopener noreferrer\");\n });\n});\n```\n\n### Interaction Tests\n\nVerify click handling and keyboard activation\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Interactions\", () => {\n it(\"calls onClick handler when a link is clicked\", async () => {\n const user = userEvent.setup();\n const handleClick = vi.fn();\n\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" onClick={handleClick}>\n General\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"link\", { name: \"General\" }));\n expect(handleClick).toHaveBeenCalled();\n });\n\n it(\"is focusable with Tab key\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n await user.tab();\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveFocus();\n });\n});\n```\n\n### Accessibility Tests\n\nVerify accessible labels and ID support\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Accessibility\", () => {\n it(\"supports aria-label on the root for landmark identification\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"navigation\", { name: \"Order navigation\" })\n ).toBeInTheDocument();\n });\n\n it(\"supports a persistent id on the root for tracking\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root id=\"order-tab-nav\" aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"navigation\")).toHaveAttribute(\n \"id\",\n \"order-tab-nav\"\n );\n });\n});\n```\n\n\n## Resources\n\n- [React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n- [ARIA: navigation landmark](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role)\n- [aria-current attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)\n- [Tabs](/components/navigation/tabs) — use when switching content panels on the same page\n",
270
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { TabNav, type TabNavProps, type TabNavItemProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\n`TabNav` is a compound component for **page-level route-based navigation** that renders a `<nav>` landmark containing `<a>` anchor elements. The active item is identified with `aria-current=\"page\"`.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## TabNav vs Tabs\n\n`TabNav` and `Tabs` look similar but serve fundamentally different purposes:\n\n| | `TabNav` | `Tabs` |\n|---|---|---|\n| **Use case** | Page-level navigation between routes | Switching content panels within a page |\n| **HTML semantics** | `<nav>` + `<a>` | `role=\"tablist\"` + `role=\"tab\"` |\n| **Active state** | `aria-current=\"page\"` | `aria-selected=\"true\"` |\n| **Keyboard** | Sequential Tab key | Arrow keys (roving tabindex) |\n| **Content** | No panel — content managed by router | Owns associated `TabPanel` |\n| **URL** | Always (`href` required) | Optional (`href` prop) |\n\nBoth `TabNav` and `Tabs` support client-side routing — the deciding factor is whether the component owns its content panels or delegates to the router. Use `TabNav` when each item navigates to a different route and the router manages what's rendered. Use `Tabs` when the content lives alongside the tab list.\n\n## Usage examples\n\n### Client-side view switching\n\nThe most common TabNav pattern: a persistent nav bar where clicking an item swaps the visible content without a full page reload. In a real app, `isCurrent` is derived from the router and the `href` triggers a route change. The example below simulates this with local state to show the interaction.\n\n```jsx live-dev\nconst items = [\n { href: '/orders/123/general', label: 'General' },\n { href: '/orders/123/items', label: 'Items' },\n { href: '/orders/123/shipping', label: 'Shipping' },\n];\n\nconst content = {\n '/orders/123/general': 'General settings and order details.',\n '/orders/123/items': 'Line items and quantities.',\n '/orders/123/shipping': 'Shipping address and carrier information.',\n};\n\nconst App = () => {\n const [activePath, setActivePath] = React.useState(items[0].href);\n\n return (\n <Stack gap=\"400\">\n <TabNav.Root aria-label=\"Order navigation\">\n {items.map((item) => (\n <TabNav.Item\n key={item.href}\n href={item.href}\n isCurrent={activePath === item.href}\n onClick={(e) => { e.preventDefault(); setActivePath(item.href); }}\n >\n {item.label}\n </TabNav.Item>\n ))}\n </TabNav.Root>\n <Box padding=\"400\">{content[activePath]}</Box>\n </Stack>\n );\n};\n```\n\n### Variants\n\nThe `variant` prop controls the visual style. Available variants: `line` (default), `rounded`, `pill`. (`Tabs` exposes the same three variants.)\n\n- **`line`** — an underline strip beneath the active item (visually twinned with the `Tabs` `line` variant).\n- **`rounded`** — a soft rounded-rect highlight behind the active item.\n- **`pill`** — a fully-rounded capsule highlight behind the active item.\n\nThe `rounded` and `pill` variants drop the baseline, add a small gap between items, and drive their active highlight off `colorPalette` (defaulting to `primary`), so they theme with the surrounding palette.\n\nThe legacy `tabs` variant name is accepted as a deprecated alias for `line`.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"600\">\n {['line', 'rounded', 'pill'].map((variant) => (\n <Stack key={variant} direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">{variant}</Text>\n <TabNav.Root aria-label={`Navigation (${variant})`} variant={variant}>\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n ))}\n </Stack>\n);\n```\n\n### Animated highlight\n\nThe active highlight always slides between items as the active item changes — a single indicator instead of a static per-item highlight. It adapts to the variant: a sliding underline bar for `line`, and a sliding filled highlight for `rounded`/`pill`. No prop is required.\n\nThe indicator is an `aria-hidden`, non-focusable element, so `aria-current`, focus rings, and keyboard order are unaffected. Its position is measured from the live DOM (`aria-current=\"page\"`) and kept in sync as the active item changes or the layout resizes. The recipe's static marker is the SSR / no-JS fallback.\n\nThe slide transition is automatically disabled under `prefers-reduced-motion: reduce` — the highlight snaps into place. (To disable the motion, the user sets the OS-level reduced-motion preference; there is no per-instance prop.)\n\n```jsx live-dev\nconst items = [\n { href: '/orders/123/general', label: 'General' },\n { href: '/orders/123/items', label: 'Items' },\n { href: '/orders/123/shipping', label: 'Shipping' },\n];\n\nconst App = () => {\n const [activePath, setActivePath] = React.useState(items[0].href);\n\n return (\n <TabNav.Root aria-label=\"Order navigation\" variant=\"rounded\">\n {items.map((item) => (\n <TabNav.Item\n key={item.href}\n href={item.href}\n isCurrent={activePath === item.href}\n onClick={(e) => { e.preventDefault(); setActivePath(item.href); }}\n >\n {item.label}\n </TabNav.Item>\n ))}\n </TabNav.Root>\n );\n};\n```\n\n### Active item\n\nMark the currently viewed page with `isCurrent`. This sets `aria-current=\"page\"` on the anchor element, which screen readers announce as \"current page\".\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n);\n```\n\nWhen using a client-side router, set `isCurrent` based on the active route:\n\n```tsx\nimport { useLocation } from 'react-router-dom';\n\nconst OrderNav = ({ orderId }: { orderId: string }) => {\n const { pathname } = useLocation();\n\n return (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item\n href={`/orders/${orderId}/general`}\n isCurrent={pathname.endsWith('/general')}\n >\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/items`}\n isCurrent={pathname.endsWith('/items')}\n >\n Items\n </TabNav.Item>\n </TabNav.Root>\n );\n};\n```\n\n### Size variants\n\nThe `size` prop controls font size and padding. Available sizes: `sm`, `md` (default), `lg`.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"800\">\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Small</Text>\n <TabNav.Root aria-label=\"Navigation (sm)\" size=\"sm\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Medium (default)</Text>\n <TabNav.Root aria-label=\"Navigation (md)\" size=\"md\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n <Stack direction=\"column\" gap=\"300\">\n <Text fontWeight=\"500\">Large</Text>\n <TabNav.Root aria-label=\"Navigation (lg)\" size=\"lg\">\n <TabNav.Item href=\"/general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </Stack>\n </Stack>\n);\n```\n\n### Disabled items\n\nUse `isDisabled` to prevent navigation to items that are not yet accessible. Disabled items are visually dimmed, removed from the tab sequence, and their `href` is stripped.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### External links\n\nUse `target` and `rel` to open a navigation item in a new browser tab. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"` for security.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item\n href=\"https://docs.example.com/orders\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Docs ↗\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### With icons\n\nTab items support inline icons alongside text via the `gap` built into the item layout.\n\n```jsx live-dev\nconst App = () => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n <Icons.Info />\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">\n <Icons.List />\n Items\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">\n <Icons.LocalShipping />\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## Component requirements\n\n### Accessibility\n\nThe TabNav component uses semantic HTML for navigation — it does not use ARIA roles for the tablist pattern. Accessibility is handled natively by the browser's `<nav>` landmark and `<a>` anchor semantics. `TabNav.Item` uses `useLink` from React Aria for consistent interaction handling.\n\n#### Role\n\n- `TabNav.Root` renders a `<nav>` HTML element — a navigation landmark, **not** `role=\"tablist\"`\n- `TabNav.Item` renders an `<a>` HTML element with the implicit `link` role — **not** `role=\"tab\"`\n- The active item uses `aria-current=\"page\"` — **not** `aria-selected`\n\n#### Labeling\n\nAlways provide an `aria-label` on `TabNav.Root` to describe the navigation region. This is especially important when a page contains multiple `<nav>` landmarks.\n\n```tsx\n<TabNav.Root aria-label=\"Order navigation\">\n ...\n</TabNav.Root>\n```\n\n#### Keyboard navigation\n\nTabNav uses standard sequential keyboard navigation — no roving tabindex, no arrow key cycling.\n\n| Key | Action |\n|-----|--------|\n| `Tab` | Move focus to the next link |\n| `Shift + Tab` | Move focus to the previous link |\n| `Enter` | Activate the focused link |\n\nArrow keys (`ArrowRight`, `ArrowLeft`) do **not** move focus between items. This is intentional — TabNav is a `<nav>` with standard anchor links, not a `role=\"tablist\"` widget.\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id:\n\n```tsx\nconst PERSISTENT_ID = \"order-detail-tab-nav\";\n\nexport const OrderTabNav = ({ orderId }: { orderId: string }) => (\n <TabNav.Root id={PERSISTENT_ID} aria-label=\"Order navigation\">\n <TabNav.Item href={`/orders/${orderId}/general`} isCurrent>\n General\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## API reference\n\n<PropsTable id=\"TabNav\" />\n\n## Common patterns\n\n### Syncing active item with router\n\nIn most routing setups you derive `isCurrent` from the current URL rather than managing it in state.\n\n```tsx\n// React Router v6\nimport { useMatch } from 'react-router-dom';\n\nconst OrderNav = ({ orderId }: { orderId: string }) => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item\n href={`/orders/${orderId}/general`}\n isCurrent={!!useMatch(`/orders/${orderId}/general`)}\n >\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/items`}\n isCurrent={!!useMatch(`/orders/${orderId}/items`)}\n >\n Items\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/shipping`}\n isCurrent={!!useMatch(`/orders/${orderId}/shipping`)}\n >\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n### Conditionally disabling items\n\nDisable items based on permissions or loading state rather than hiding them — this preserves the layout and communicates to users that the section exists.\n\n```tsx\nconst OrderNav = ({ orderId, canViewShipping }: { orderId: string; canViewShipping: boolean }) => (\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href={`/orders/${orderId}/general`} isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item\n href={`/orders/${orderId}/shipping`}\n isDisabled={!canViewShipping}\n >\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n);\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using TabNav within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the TabNav component renders a nav landmark with anchor links\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Basic rendering\", () => {\n it(\"renders a navigation landmark with links\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"navigation\", { name: \"Order navigation\" })\n ).toBeInTheDocument();\n expect(screen.getAllByRole(\"link\")).toHaveLength(3);\n });\n\n it(\"renders items as anchor elements with correct href\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveAttribute(\n \"href\",\n \"/orders/123/general\"\n );\n expect(screen.getByRole(\"link\", { name: \"Items\" })).toHaveAttribute(\n \"href\",\n \"/orders/123/items\"\n );\n });\n});\n```\n\n### Active Item Tests\n\nVerify aria-current=\"page\" is applied correctly to the active navigation item\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Active item\", () => {\n it(\"sets aria-current='page' on the active item\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveAttribute(\n \"aria-current\",\n \"page\"\n );\n });\n\n it(\"does not set aria-current on inactive items\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"/orders/123/items\">Items</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"Items\" })).not.toHaveAttribute(\n \"aria-current\"\n );\n expect(screen.getByRole(\"link\", { name: \"Shipping\" })).not.toHaveAttribute(\n \"aria-current\"\n );\n });\n\n it(\"does not use aria-selected (navigation, not widget)\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"link\", { name: \"General\" })).not.toHaveAttribute(\n \"aria-selected\"\n );\n });\n});\n```\n\n### Disabled Item Tests\n\nVerify disabled items are marked inaccessible and removed from tab order\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Disabled items\", () => {\n it(\"marks disabled items with aria-disabled='true'\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n const disabledItem = screen.getByRole(\"link\", { name: \"Shipping\" });\n expect(disabledItem).toHaveAttribute(\"aria-disabled\", \"true\");\n });\n\n it(\"disabled items have no tabIndex (excluded from tab sequence by browser)\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n <TabNav.Item href=\"/orders/123/shipping\" isDisabled>\n Shipping\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n // React Aria sets no tabIndex on the disabled anchor — the browser excludes it\n const disabledItem = screen.getByRole(\"link\", { name: \"Shipping\" });\n expect(disabledItem).not.toHaveAttribute(\"tabindex\");\n });\n});\n```\n\n### External Link Tests\n\nVerify target and rel attributes for external navigation items\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - External links\", () => {\n it(\"applies target and rel to external link items\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item\n href=\"https://docs.example.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Docs ↗\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n const externalLink = screen.getByRole(\"link\", { name: \"Docs ↗\" });\n expect(externalLink).toHaveAttribute(\"target\", \"_blank\");\n expect(externalLink).toHaveAttribute(\"rel\", \"noopener noreferrer\");\n });\n});\n```\n\n### Interaction Tests\n\nVerify click handling and keyboard activation\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Interactions\", () => {\n it(\"calls onClick handler when a link is clicked\", async () => {\n const user = userEvent.setup();\n const handleClick = vi.fn();\n\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\" onClick={handleClick}>\n General\n </TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"link\", { name: \"General\" }));\n expect(handleClick).toHaveBeenCalled();\n });\n\n it(\"is focusable with Tab key\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n await user.tab();\n expect(screen.getByRole(\"link\", { name: \"General\" })).toHaveFocus();\n });\n});\n```\n\n### Accessibility Tests\n\nVerify accessible labels and ID support\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { TabNav, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"TabNav - Accessibility\", () => {\n it(\"supports aria-label on the root for landmark identification\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"navigation\", { name: \"Order navigation\" })\n ).toBeInTheDocument();\n });\n\n it(\"supports a persistent id on the root for tracking\", () => {\n render(\n <NimbusProvider>\n <TabNav.Root id=\"order-tab-nav\" aria-label=\"Order navigation\">\n <TabNav.Item href=\"/orders/123/general\">General</TabNav.Item>\n </TabNav.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"navigation\")).toHaveAttribute(\n \"id\",\n \"order-tab-nav\"\n );\n });\n});\n```\n\n\n## Resources\n\n- [React Aria useLink](https://react-spectrum.adobe.com/react-aria/useLink.html)\n- [ARIA: navigation landmark](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role)\n- [aria-current attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)\n- [Tabs](/components/navigation/tabs) — use when switching content panels on the same page\n",
227
271
  "toc": [
228
272
  {
229
273
  "value": "Getting started",
@@ -288,6 +332,28 @@
288
332
  ],
289
333
  "parent": "root"
290
334
  },
335
+ {
336
+ "value": "Variants",
337
+ "href": "#variants",
338
+ "depth": 3,
339
+ "numbering": [
340
+ 1,
341
+ 3,
342
+ 2
343
+ ],
344
+ "parent": "root"
345
+ },
346
+ {
347
+ "value": "Animated highlight",
348
+ "href": "#animated-highlight",
349
+ "depth": 3,
350
+ "numbering": [
351
+ 1,
352
+ 3,
353
+ 3
354
+ ],
355
+ "parent": "root"
356
+ },
291
357
  {
292
358
  "value": "Active item",
293
359
  "href": "#active-item",
@@ -295,7 +361,7 @@
295
361
  "numbering": [
296
362
  1,
297
363
  3,
298
- 2
364
+ 4
299
365
  ],
300
366
  "parent": "root"
301
367
  },
@@ -306,7 +372,7 @@
306
372
  "numbering": [
307
373
  1,
308
374
  3,
309
- 3
375
+ 5
310
376
  ],
311
377
  "parent": "root"
312
378
  },
@@ -317,7 +383,7 @@
317
383
  "numbering": [
318
384
  1,
319
385
  3,
320
- 4
386
+ 6
321
387
  ],
322
388
  "parent": "root"
323
389
  },
@@ -328,7 +394,7 @@
328
394
  "numbering": [
329
395
  1,
330
396
  3,
331
- 5
397
+ 7
332
398
  ],
333
399
  "parent": "root"
334
400
  },
@@ -339,7 +405,7 @@
339
405
  "numbering": [
340
406
  1,
341
407
  3,
342
- 6
408
+ 8
343
409
  ],
344
410
  "parent": "root"
345
411
  },