NavigationMenu

Always-rendered nested link list with current-page marking.

organismsSource
import { NavigationMenu } from '@elirobinson/react/components/organisms/NavigationMenu';

Styles: @elirobinson/react/styles/organisms/NavigationMenu.css — already included when you import @elirobinson/react/styles.css.

Show code
import { NavigationMenu } from '@elirobinson/react/components/organisms/NavigationMenu';

export default function Basic() {
  return (
    <NavigationMenu
      className="demo-col"
      currentPath="/apps/kids-recipes"
      items={[
        { label: 'Home', href: '/' },
        {
          label: 'Apps',
          href: '/apps',
          items: [
            { label: 'Kids Recipes', href: '/apps/kids-recipes' },
            { label: 'Interactive Maths', href: '/apps/interactive-maths' },
          ],
        },
        { label: 'Coaching Guides', href: '/coaching-guides' },
        { label: 'About', href: '/about' },
      ]}
    />
  );
}

When to use it

Use NavigationMenu for a site's real navigation — every item renders as a genuine <a>, so it works with middle-click, cmd-click, and JavaScript disabled. It supports one level of nested links (a section with sub-pages), but the nested items are always rendered inline, not hidden behind a hover or click. If you need a menu that opens and closes on demand, that's DropdownMenu, not this.

Without a current page

currentPath is optional. When it's omitted, or when it matches nothing in items, no item is marked current — none gets aria-current.

Show code
import { NavigationMenu } from '@elirobinson/react/components/organisms/NavigationMenu';

export default function NoCurrentPath() {
  return (
    <NavigationMenu
      className="demo-col"
      items={[
        { label: 'AI consulting', href: '/ai-consulting' },
        { label: 'Tech support', href: '/tech-support' },
        { label: 'Contact', href: '/contact' },
      ]}
    />
  );
}

Section labels

href is optional. Omit it when a group's title is a heading rather than a page — "Foundations" above a list of foundation pages — and the item renders as an inert <span> instead of an <a>: not focusable, not a navigation target, never marked current. The label also names its nested list through aria-labelledby, so a screen reader announces "Foundations, list, 7 items" on descending into the group.

Don't reach for the group's first child href to satisfy the type. That makes the header match currentPath whenever the child is open, so the header renders as the current page alongside it and a second aria-current="page" appears in the tree.

Show code
import { NavigationMenu } from '@elirobinson/react/components/organisms/NavigationMenu';

export default function SectionLabels() {
  return (
    <NavigationMenu
      className="demo-col"
      currentPath="/foundations/color"
      items={[
        {
          label: 'Foundations',
          items: [
            { label: 'Color', href: '/foundations/color' },
            { label: 'Typography', href: '/foundations/typography' },
          ],
        },
        {
          label: 'Atoms',
          items: [
            { label: 'Avatar', href: '/components/avatar' },
            { label: 'Button', href: '/components/button' },
          ],
        },
      ]}
    />
  );
}

Props

PropTypeDefaultDescription
itemsrequiredNavigationMenuItem[]
currentPathstring

Also accepts all HTMLAttributes<HTMLElement> props.

Accessibility

  • Renders a <nav> landmark wrapping a plain, always-rendered nested <ul>/<li>/<a> tree — not a disclosure widget. Nothing is collapsed, hidden, or toggled.
  • Every item with an href is a real <a href>, so Tab/Shift+Tab already walk the entire tree, including nested items, in document order — zero custom key handling. An item without an href is a <span> label, so it's skipped by Tab — there's nothing there to activate.
  • The item matching currentPath gets aria-current="page"; every other item has the attribute omitted entirely (not set to "false"). Nothing is marked current if currentPath matches nothing.
  • Deliberately has no Escape handling and no aria-expanded/aria-controls anywhere — there's no toggleable submenu here to escape out of or expand. That's an explicit design choice, not an oversight: if you need a collapsible or flyout submenu, this isn't that component.
  • Nested items render inside their parent <li> as their own <ul>, so a screen reader announces "list, N items" once at the top level and again on descending into a nested group, rather than one flattened list.
  • A nested list under a hrefless label is named after that label via aria-labelledby, so the group announces as "Foundations, list, N items". A nested list under a link parent stays unnamed — the link is a destination, not a title for the list beneath it.

Do

  • Pass real, working hrefs — this is a navigation list, not a menu of actions.
  • Omit href on a group title that names its children but isn’t a page of its own.
  • Keep currentPath in sync with the router so aria-current always matches what’s on screen.
  • Nest one level deep in practice — the recursive items field supports more, but three-plus levels are hard to scan visually.

Don't

  • Use it for a flyout or disclosure submenu that opens on click or hover — reach for DropdownMenu for that toggle behavior.
  • Attach onClick handlers that prevent navigation — every link should be genuinely middle-click-able.
  • Give a section header its first child’s href — the header then renders as current whenever that child is open.
  • Expect aria-expanded or collapse behavior — nested items are always rendered and always in the tab order.