# NavMenu

```tsx
import { NavMenu } from '@tangible/ui';
```

Interactive docs and examples: [NavMenu in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/components-navmenu--docs).

## NavMenu props

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |
| `flyoutTrigger?` | `"click" \| "hover"` | `'click'` | How a horizontal bar's flyouts open. `click` (default): click or Enter/Space. `hover`: also opens for a mouse once the pointer settles on an item (hover intent, as in WordPress's Toolbar: under 6px of movement in 100ms), so sweeping across the bar opens nothing. Once primed (a panel is open, or one just closed), the next opens at once. There's a grace period for crossing into the panel. Escape dismisses it until the pointer leaves (WCAG 1.4.13); clicking pins it open. Touch and keyboard behave as `click` either way. |
| `labels?` | `NavMenuLabels` | | Visually hidden strings. |
| `orientation?` | `"horizontal" \| "vertical"` | `'vertical'` | `vertical` (sidebars) stacks groups and nests a `NavMenu.Sub` inline. `horizontal` makes a bar: groups become inline clusters (headings visually hidden, lists still labelled), and an item with a `NavMenu.Sub` opens it as a flyout. A flyout follows the APG disclosure pattern: a button with `aria-expanded`, opened by click or Enter/Space (never hover), closed by Escape (focus returns to the button), Tab-ing out or clicking elsewhere. One flyout is open at a time. Groups inside a flyout show their headings. |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### NavMenu.Group

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |
| `label?` | `ReactNode` | | Visible group heading, styled with the overline text style. It names the group's list via `aria-labelledby`. Omit it for an unlabelled group. |
| `labelAs?` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "span"` | `'h2'` | Element for the heading. Pick the level that fits the page outline; use `'span'` when headings would be noise (the list stays labelled either way). |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### NavMenu.Item

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | The label, plus an optional `NavMenu.Sub` of nested items. Vertical: the Sub renders after the link, inside the same list item, and whether it renders at all is up to you (e.g. only while its parent is current). Horizontal: the Sub is a flyout. With no `href`, the row itself toggles it; with an `href` (or `asChild` link), the row navigates and a separate toggle button opens it. |
| `asChild?` | `true` | | Only in some variants; the others reject it. |
| `current?` | `"date" \| "location" \| "page" \| "step" \| "time" \| "true" \| boolean` | | Marks the current item; see `NavMenuCurrent`. |
| `disabled?` | `boolean` | | Inert item: rendered as a disabled link (`role="link"`, `aria-disabled`, no href), out of the tab order. For destinations that exist but aren't available yet. |
| `href?` | `string` | | Only in some variants; the others reject it. |
| `icon?` | `IconName` | | Leading icon, from the registry. Sized by `--tui-nav-menu-icon-size`. |
| `prefix?` | `ReactNode` | | Custom leading content in the icon's place (an avatar, a glyph not in the registry). Replaces `icon` when both are set. Unlike `icon`, it isn't hidden from assistive tech: mark decorative content `aria-hidden` yourself, or its text joins the name. A shape drawn with CSS `background` disappears in forced colors; give it a border if it carries meaning. |
| `suffix?` | `ReactNode` | | Trailing content pushed to the row's inline end: a badge, a count. It sits inside the row, so its text joins the accessible name: give a bare number context, so `3 <span class="tui-visually-hidden">unread</span>` reads "Inbox 3 unread" (put the space outside the hidden span). Must not be interactive (no buttons or links): a control nested in a link is invalid. Both slots reach the name only through the row's content, so don't pass `aria-label`/`aria-labelledby` to an item with a slot: either replaces the whole name. On a row that is itself the flyout's disclosure button, the suffix is part of the button's name; on a split row it belongs to the link (the destination), and the toggle stays "Reports submenu". |

Also accepts the standard attributes of `AnchorHTMLAttributes` and `ButtonHTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### NavMenu.Sub

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

## Other exports

- `defaultNavMenuLabels`: `Required<NavMenuLabels>`

## Examples

- [Basic](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--basic): Groups with overline headings, icons, and the current page. The menu sits
  inside `Sidebar.Nav`, which is the landmark and owns the label.

- [Nested](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--nested): One level of nesting. The open parent is an ancestor of the current page,
  so it carries `current="true"` (emphasis, no fill) and the child is the
  page. Here the sub list renders only while its parent is open, as in the
  staff admin shell, so a closed group adds no tab stops.

- [With a router Link (asChild)](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--with-router-link): `asChild` hands the row to your router's `<Link>`. The item supplies the
  class, `aria-current`, icon and label wrapper; the link keeps its own props.

- [Configured (indicator bar, tokens)](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--configured): Everything is a knob. The leading bar is opt-in (`--tui-nav-menu-indicator-width`),
  and the group heading is the overline text style, so its knobs apply too.

- [Slots](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--slots): `suffix` puts a badge or count at the row's inline end; `prefix` replaces
  the icon with custom leading content. Both sit inside the link, so their
  text joins its accessible name: a bare count gets visually hidden context
  ("Inbox 12 unread"), with the space outside the hidden span. Slots must not
  be interactive; a dev warning fires if they are.

- [Level knobs (per-state, per-level)](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--level-knobs): Every row state is a knob (`--tui-nav-menu-item-{bg,fg}`, `-hover`,
  `-active`, `-current`, `-ancestor`, plus weights), and each level can
  override it: `--tui-nav-menu-sub-item-*` for nested rows,
  `--tui-nav-menu-bar-item-*` for top-level bar rows,
  `--tui-nav-menu-flyout-item-*` for rows in a flyout. A level falls back to
  the general knob, so one setting restyles every level. Here the general
  hover is warning-tinted and nested rows override it with info.

  Colours for a dark header bar belong on `--tui-nav-menu-bar-item-*`: the
  general knob also reaches the flyout rows, which sit on the light panel.

- [Button Items](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--button-items): Items with no `href` render as buttons, for rows that change state in place
  (a section switcher, say). An unlabelled group is a plain list with no
  heading.

- [Keyboard](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--keyboard): Keyboard: rows are ordinary links in tab order, with an inset focus ring.

- [Horizontal](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--horizontal): `orientation="horizontal"`: a bar. Items look like the vertical rows (fill
  for the current page); the underline is opt-in, see HorizontalIndicator.
  Groups become inline clusters whose labels are visually hidden but still
  name their lists.

- [Horizontal with flyouts](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--horizontal-flyouts): Flyouts follow the APG disclosure navigation pattern, not `role="menu"`:
  - **Parent with no `href` ("Courses"):** the row is the toggle button.
  - **Parent with an `href` or a router link ("Reports"):** the row
    navigates, and a separate toggle, named "Reports submenu", opens it.
  - **Groups** inside a flyout show their headings.
  - **Opening and closing:** click or Enter/Space opens, never hover. Escape
    closes and returns focus, and so do Tab-ing out, clicking elsewhere and
    following a link inside. One panel is open at a time.

- [Horizontal with underline indicator](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--horizontal-indicator): The underline is the same knob as the vertical leading bar,
  `--tui-nav-menu-indicator-width`; in a bar it draws under the current item,
  or under the ancestor whose page is in a flyout.

- [Horizontal, open on hover](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--horizontal-hover): `flyoutTrigger="hover"`: a mouse opens a flyout once the pointer settles on
  an item (hover intent, as in WordPress's Toolbar), so sweeping across the bar
  opens nothing. Once primed, the next opens at once. A grace period lets the
  pointer cross into the panel. Built to
  WCAG 1.4.13: Escape dismisses it and it stays closed until the pointer
  leaves. Clicking pins it open. Touch and keyboard behave as `click`.

- [Flyouts at the edges](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--flyout-at-edges): Flyouts at the bar's edges stay inside the viewport: Floating UI shifts a
  panel that would overflow. Parents sit at the far left and right here.

- [Flyout with nested levels](https://storybook-tangible-ui.pages.dev/?path=/story/components-navmenu--flyout-nesting): Deeper levels: inside a flyout, an item's own Sub nests inline (indented,
  with a guide line), to any depth. Cascading flyouts aren't supported.

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-nav-menu-group-gap`
- `--tui-nav-menu-item-gap`
- `--tui-nav-menu-item-height`
- `--tui-nav-menu-item-padding-inline`
- `--tui-nav-menu-item-radius`
- `--tui-nav-menu-item-font-size`
- `--tui-nav-menu-item-bg`
- `--tui-nav-menu-item-fg`
- `--tui-nav-menu-item-weight`
- `--tui-nav-menu-item-bg-hover`
- `--tui-nav-menu-item-fg-hover`
- `--tui-nav-menu-item-bg-active`
- `--tui-nav-menu-item-fg-active`
- `--tui-nav-menu-item-bg-current`
- `--tui-nav-menu-item-fg-current`
- `--tui-nav-menu-item-weight-current`
- `--tui-nav-menu-item-bg-ancestor`
- `--tui-nav-menu-item-fg-ancestor`
- `--tui-nav-menu-item-weight-ancestor`
- `--tui-nav-menu-item-fg-disabled`
- `--tui-nav-menu-icon-size`
- `--tui-nav-menu-indicator-width`
- `--tui-nav-menu-indicator-color`
- `--tui-nav-menu-sub-indent`
- `--tui-nav-menu-sub-border`
- `--tui-nav-menu-bar-gap`
- `--tui-nav-menu-flyout-bg`
- `--tui-nav-menu-flyout-fg`
- `--tui-nav-menu-flyout-border`
- `--tui-nav-menu-flyout-radius`
- `--tui-nav-menu-flyout-shadow`
- `--tui-nav-menu-flyout-min-width`
- `--tui-nav-menu-sub-item-bg`
- `--tui-nav-menu-sub-item-fg`
- `--tui-nav-menu-sub-item-weight`
- `--tui-nav-menu-sub-item-bg-hover`
- `--tui-nav-menu-sub-item-fg-hover`
- `--tui-nav-menu-sub-item-bg-active`
- `--tui-nav-menu-sub-item-fg-active`
- `--tui-nav-menu-sub-item-bg-current`
- `--tui-nav-menu-sub-item-fg-current`
- `--tui-nav-menu-sub-item-weight-current`
- `--tui-nav-menu-sub-item-bg-ancestor`
- `--tui-nav-menu-sub-item-fg-ancestor`
- `--tui-nav-menu-sub-item-weight-ancestor`
- `--tui-nav-menu-sub-item-fg-disabled`
- `--tui-nav-menu-bar-item-bg`
- `--tui-nav-menu-bar-item-fg`
- `--tui-nav-menu-bar-item-weight`
- `--tui-nav-menu-bar-item-bg-hover`
- `--tui-nav-menu-bar-item-fg-hover`
- `--tui-nav-menu-bar-item-bg-active`
- `--tui-nav-menu-bar-item-fg-active`
- `--tui-nav-menu-bar-item-bg-current`
- `--tui-nav-menu-bar-item-fg-current`
- `--tui-nav-menu-bar-item-weight-current`
- `--tui-nav-menu-bar-item-bg-ancestor`
- `--tui-nav-menu-bar-item-fg-ancestor`
- `--tui-nav-menu-bar-item-weight-ancestor`
- `--tui-nav-menu-bar-item-fg-disabled`
- `--tui-nav-menu-flyout-item-bg`
- `--tui-nav-menu-flyout-item-fg`
- `--tui-nav-menu-flyout-item-weight`
- `--tui-nav-menu-flyout-item-bg-hover`
- `--tui-nav-menu-flyout-item-fg-hover`
- `--tui-nav-menu-flyout-item-bg-active`
- `--tui-nav-menu-flyout-item-fg-active`
- `--tui-nav-menu-flyout-item-bg-current`
- `--tui-nav-menu-flyout-item-fg-current`
- `--tui-nav-menu-flyout-item-weight-current`
- `--tui-nav-menu-flyout-item-bg-ancestor`
- `--tui-nav-menu-flyout-item-fg-ancestor`
- `--tui-nav-menu-flyout-item-weight-ancestor`
- `--tui-nav-menu-flyout-item-fg-disabled`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/components-navmenu--docs
