@commercetools/nimbus-mcp 3.1.0 → 3.3.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 (202) hide show
  1. package/README.md +9 -1
  2. package/data/docs/route-manifest.json +806 -41
  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-feedback-activity-indicator.json +396 -0
  8. package/data/docs/routes/components-feedback-tooltip.json +23 -1
  9. package/data/docs/routes/components-inputs-drop-zone.json +562 -0
  10. package/data/docs/routes/components-inputs-file-trigger.json +421 -0
  11. package/data/docs/routes/components-layout-splitter.json +3 -3
  12. package/data/docs/routes/components-media-avatar.json +16 -5
  13. package/data/docs/routes/components-navigation-menu.json +1 -1
  14. package/data/docs/routes/components-navigation-tabnav.json +80 -14
  15. package/data/docs/routes/components-navigation-tabs.json +44 -11
  16. package/data/docs/routes/components-navigation-tree.json +445 -0
  17. package/data/docs/routes/home-getting-started-mcp-server-overview.json +2 -2
  18. package/data/docs/routes/home-getting-started-mcp-server-setup.json +2 -2
  19. package/data/docs/routes/patterns-buttons-floating-action-button.json +392 -0
  20. package/data/docs/routes/patterns-buttons.json +58 -0
  21. package/data/docs/search-index.json +1 -1
  22. package/data/docs/types/AccordionContent.json +27 -27
  23. package/data/docs/types/AccordionHeader.json +89 -89
  24. package/data/docs/types/AccordionItem.json +22 -22
  25. package/data/docs/types/AccordionRoot.json +14 -14
  26. package/data/docs/types/ActivityIndicator.json +202 -0
  27. package/data/docs/types/AlertDescription.json +8 -8
  28. package/data/docs/types/AlertDismissButton.json +98 -81
  29. package/data/docs/types/AlertTitle.json +8 -8
  30. package/data/docs/types/Avatar.json +19 -2
  31. package/data/docs/types/Badge.json +2 -2
  32. package/data/docs/types/Body.json +6 -6
  33. package/data/docs/types/Box.json +6 -6
  34. package/data/docs/types/Button.json +106 -89
  35. package/data/docs/types/Calendar.json +56 -56
  36. package/data/docs/types/Caption.json +6 -6
  37. package/data/docs/types/CardRoot.json +2 -2
  38. package/data/docs/types/Cell.json +20 -20
  39. package/data/docs/types/ChatMessage.json +12 -0
  40. package/data/docs/types/ChatMessageActions.json +42 -0
  41. package/data/docs/types/ChatMessageAvatar.json +140 -0
  42. package/data/docs/types/ChatMessageBody.json +42 -0
  43. package/data/docs/types/ChatMessageList.json +12 -0
  44. package/data/docs/types/ChatMessageListItem.json +42 -0
  45. package/data/docs/types/ChatMessageListRoot.json +78 -0
  46. package/data/docs/types/ChatMessageMeta.json +42 -0
  47. package/data/docs/types/ChatMessageRoot.json +119 -0
  48. package/data/docs/types/ChatMessageTyping.json +42 -0
  49. package/data/docs/types/Checkbox.json +88 -88
  50. package/data/docs/types/Code.json +11 -11
  51. package/data/docs/types/CollapsibleMotionContent.json +2 -2
  52. package/data/docs/types/CollapsibleMotionRoot.json +2 -2
  53. package/data/docs/types/CollapsibleMotionTrigger.json +4 -4
  54. package/data/docs/types/Column.json +8 -8
  55. package/data/docs/types/ColumnGroup.json +8 -8
  56. package/data/docs/types/ColumnHeader.json +18 -18
  57. package/data/docs/types/ComboBoxListBox.json +70 -70
  58. package/data/docs/types/ComboBoxOption.json +64 -64
  59. package/data/docs/types/ComboBoxPopover.json +91 -70
  60. package/data/docs/types/ComboBoxRoot.json +8 -8
  61. package/data/docs/types/ComboBoxSection.json +25 -25
  62. package/data/docs/types/ComboBoxTrigger.json +6 -6
  63. package/data/docs/types/Content.json +2 -2
  64. package/data/docs/types/DataTable.json +2 -2
  65. package/data/docs/types/DataTableBody.json +21 -21
  66. package/data/docs/types/DataTableHeader.json +27 -27
  67. package/data/docs/types/DataTableRoot.json +2 -2
  68. package/data/docs/types/DataTableTable.json +20 -20
  69. package/data/docs/types/DateInput.json +73 -73
  70. package/data/docs/types/DatePicker.json +58 -58
  71. package/data/docs/types/DateRangePicker.json +86 -86
  72. package/data/docs/types/DateRangePickerField.json +86 -86
  73. package/data/docs/types/DefaultPageBackLink.json +16 -16
  74. package/data/docs/types/DefaultPageRoot.json +2 -2
  75. package/data/docs/types/DialogCloseTrigger.json +96 -79
  76. package/data/docs/types/DialogTrigger.json +2 -2
  77. package/data/docs/types/DraggableListField.json +88 -60
  78. package/data/docs/types/DraggableListItem.json +53 -53
  79. package/data/docs/types/DraggableListRoot.json +88 -60
  80. package/data/docs/types/DrawerCloseTrigger.json +96 -79
  81. package/data/docs/types/DrawerTrigger.json +2 -2
  82. package/data/docs/types/DropZone.json +161 -0
  83. package/data/docs/types/FieldErrors.json +2 -2
  84. package/data/docs/types/FileTrigger.json +130 -0
  85. package/data/docs/types/Flex.json +22 -22
  86. package/data/docs/types/FloatingActionButton.json +1036 -0
  87. package/data/docs/types/Footer.json +6 -6
  88. package/data/docs/types/FormActionBar.json +1 -1
  89. package/data/docs/types/FormFieldRoot.json +2 -2
  90. package/data/docs/types/Grid.json +24 -24
  91. package/data/docs/types/Group.json +12 -12
  92. package/data/docs/types/Header.json +6 -6
  93. package/data/docs/types/Heading.json +8 -8
  94. package/data/docs/types/Icon.json +4 -4
  95. package/data/docs/types/IconButton.json +106 -89
  96. package/data/docs/types/IconToggleButton.json +73 -73
  97. package/data/docs/types/Image.json +63 -63
  98. package/data/docs/types/Indicator.json +6 -6
  99. package/data/docs/types/InlineSvg.json +2 -2
  100. package/data/docs/types/Item.json +6 -6
  101. package/data/docs/types/Kbd.json +8 -8
  102. package/data/docs/types/Link.json +30 -30
  103. package/data/docs/types/ListIndicator.json +6 -6
  104. package/data/docs/types/ListItem.json +6 -6
  105. package/data/docs/types/ListRoot.json +11 -11
  106. package/data/docs/types/LoadingSpinner.json +2 -2
  107. package/data/docs/types/MakeElementFocusable.json +18 -18
  108. package/data/docs/types/Markdown.json +183 -0
  109. package/data/docs/types/MenuItem.json +63 -63
  110. package/data/docs/types/MenuRoot.json +57 -57
  111. package/data/docs/types/MenuSection.json +31 -31
  112. package/data/docs/types/MenuSubmenuTrigger.json +4 -4
  113. package/data/docs/types/MenuTrigger.json +89 -89
  114. package/data/docs/types/MultilineTextInput.json +104 -104
  115. package/data/docs/types/MultilineTextInputField.json +102 -102
  116. package/data/docs/types/NumberInput.json +86 -86
  117. package/data/docs/types/NumberInputField.json +82 -82
  118. package/data/docs/types/PageContentColumn.json +6 -6
  119. package/data/docs/types/PageContentRoot.json +6 -6
  120. package/data/docs/types/PasswordInput.json +100 -100
  121. package/data/docs/types/PasswordInputField.json +100 -100
  122. package/data/docs/types/ProgressBar.json +13 -13
  123. package/data/docs/types/RadioInputOption.json +56 -56
  124. package/data/docs/types/RadioInputRoot.json +48 -48
  125. package/data/docs/types/RangeCalendar.json +58 -58
  126. package/data/docs/types/Region.json +6 -6
  127. package/data/docs/types/RegionTarget.json +6 -6
  128. package/data/docs/types/RichTextInput.json +2 -2
  129. package/data/docs/types/Root.json +11 -11
  130. package/data/docs/types/Row.json +6 -6
  131. package/data/docs/types/ScrollArea.json +2 -2
  132. package/data/docs/types/SearchInput.json +106 -106
  133. package/data/docs/types/SearchInputField.json +102 -102
  134. package/data/docs/types/SelectOption.json +54 -54
  135. package/data/docs/types/SelectOptionGroup.json +18 -18
  136. package/data/docs/types/SelectOptions.json +64 -64
  137. package/data/docs/types/SelectRoot.json +92 -92
  138. package/data/docs/types/Separator.json +4 -4
  139. package/data/docs/types/SimpleGrid.json +28 -28
  140. package/data/docs/types/SlidingIndicatorGeometry.json +9 -0
  141. package/data/docs/types/SlidingIndicatorRects.json +9 -0
  142. package/data/docs/types/SplitButton.json +16 -20
  143. package/data/docs/types/SplitterRoot.json +2 -2
  144. package/data/docs/types/Stack.json +2 -2
  145. package/data/docs/types/StepsNextTrigger.json +2 -2
  146. package/data/docs/types/StepsPrevTrigger.json +2 -2
  147. package/data/docs/types/StepsRoot.json +2 -2
  148. package/data/docs/types/StepsTrigger.json +2 -2
  149. package/data/docs/types/Switch.json +36 -36
  150. package/data/docs/types/TabNavItem.json +21 -21
  151. package/data/docs/types/TabNavRoot.json +5 -5
  152. package/data/docs/types/TableBody.json +6 -6
  153. package/data/docs/types/TableCaption.json +6 -6
  154. package/data/docs/types/TableCell.json +20 -20
  155. package/data/docs/types/TableColumn.json +8 -8
  156. package/data/docs/types/TableColumnGroup.json +8 -8
  157. package/data/docs/types/TableColumnHeader.json +18 -18
  158. package/data/docs/types/TableFooter.json +6 -6
  159. package/data/docs/types/TableHeader.json +6 -6
  160. package/data/docs/types/TableRoot.json +33 -33
  161. package/data/docs/types/TableRow.json +6 -6
  162. package/data/docs/types/TableScrollArea.json +6 -6
  163. package/data/docs/types/TabsList.json +2 -2
  164. package/data/docs/types/TabsPanel.json +2 -2
  165. package/data/docs/types/TabsPanels.json +2 -2
  166. package/data/docs/types/TabsRoot.json +3 -3
  167. package/data/docs/types/TabsTab.json +4 -4
  168. package/data/docs/types/TagGroupRoot.json +24 -24
  169. package/data/docs/types/TagGroupTag.json +56 -56
  170. package/data/docs/types/TagGroupTagList.json +15 -15
  171. package/data/docs/types/Text.json +8 -8
  172. package/data/docs/types/TextInput.json +102 -102
  173. package/data/docs/types/TextInputField.json +100 -100
  174. package/data/docs/types/TimeInput.json +68 -68
  175. package/data/docs/types/ToggleButton.json +75 -75
  176. package/data/docs/types/ToggleButtonGroupButton.json +32 -32
  177. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  178. package/data/docs/types/Toolbar.json +9 -9
  179. package/data/docs/types/TooltipContent.json +28 -28
  180. package/data/docs/types/TooltipRoot.json +16 -16
  181. package/data/docs/types/Tree.json +12 -0
  182. package/data/docs/types/TreeIndicator.json +828 -0
  183. package/data/docs/types/TreeItem.json +559 -0
  184. package/data/docs/types/TreeItemContent.json +27 -0
  185. package/data/docs/types/TreeRoot.json +636 -0
  186. package/data/docs/types/TreeSubTree.json +40 -0
  187. package/data/docs/types/Trigger.json +4 -4
  188. package/data/docs/types/VisuallyHidden.json +6 -6
  189. package/data/docs/types/createItemsFromCsvDrop.json +4 -4
  190. package/data/docs/types/createItemsFromDirectoryDrop.json +4 -4
  191. package/data/docs/types/createItemsFromFileDrop.json +4 -4
  192. package/data/docs/types/createItemsFromImageDrop.json +4 -4
  193. package/data/docs/types/createItemsFromJsonDrop.json +4 -4
  194. package/data/docs/types/manifest.json +26 -0
  195. package/data/docs/types/useFocusInputOnFieldClick.json +11 -0
  196. package/data/docs/types/useRegion.json +2 -2
  197. package/data/docs/types/useSlidingIndicator.json +145 -0
  198. package/data/docs/types/useTree.json +267 -0
  199. package/data/tokens.json +21 -0
  200. package/data/uikit-tokens.json +894 -0
  201. package/dist/index.js +3298 -109
  202. package/package.json +8 -6
@@ -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
  },