NavMenu
import { NavMenu } from '@tangible/ui';
Interactive docs and examples: NavMenu in Storybook.
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: Groups with overline headings, icons, and the current page. The menu sits inside
Sidebar.Nav, which is the landmark and owns the label. -
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):
asChildhands 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): 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:
suffixputs a badge or count at the row's inline end;prefixreplaces 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): 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: Items with no
hrefrender as buttons, for rows that change state in place (a section switcher, say). An unlabelled group is a plain list with no heading. -
Keyboard: Keyboard: rows are ordinary links in tab order, with an inset focus ring.
-
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: Flyouts follow the APG disclosure navigation pattern, not
role="menu":- Parent with no
href("Courses"): the row is the toggle button. - Parent with an
hrefor 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.
- Parent with no
-
Horizontal with underline 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:
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 asclick. -
Flyouts at the 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: 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