# Navigation Menu

> A family of composable components for authored navigation with dropdown panels, keyboard support, animated viewport modes, and an optional mobile drawer helper that mirrors the desktop menu.

# Navigation Menu

## Overview

Use the Navigation Menu family when you need authored navigation items with optional dropdown panels, keyboard navigation, animated viewport transitions, and a mobile drawer helper that automatically derives its structure from the desktop menu. The desktop family is composed of six components that nest together, plus a separate mobile helper and an explicit mobile-only node for complex mega-menu content.

The root component manages open state, keyboard navigation, and optional animated viewport behavior. Navigation Menu Mobile is a **separate** component placed elsewhere on the page — it connects to the desktop menu by a shared `menuId` and builds a drawer-based mobile experience at runtime.

## Authoring Structure

The desktop menu follows a strict nesting hierarchy:

```
Navigation Menu (desktop root)
└── Navigation Menu List
    ├── Navigation Menu Item
    │   ├── plain link
    │   ├── OR Navigation Menu Trigger + Navigation Menu Content
    │   │   └── optional Navigation Menu Mobile Item subtree inside content
    │   └── OR link + Navigation Menu Trigger + Navigation Menu Content
    └── Navigation Menu Item ...

Navigation Menu Mobile (separate — anywhere on the page)
```

### Desktop and Mobile Are Separate

Navigation Menu Mobile is **not** nested inside Navigation Menu. It is placed as a sibling element elsewhere on the page (typically inside the header shell alongside the desktop navigation). Both components share the same `menuId` value so the mobile helper can find and mirror the desktop structure at runtime.

In header patterns, the typical structure is:

```
Header Shell
├── Brand
├── Navigation Menu (desktop, menuId="primary-nav")
│   └── Navigation Menu List ...
├── Actions
└── Navigation Menu Mobile (separate, menuId="primary-nav")
```

### Placement Rules

| Component | Placement | Role |
|---|---|---|
| **Navigation Menu** | Top-level wrapper for the desktop navigation. | Owns orientation, open-state coordination, viewport behavior, and the menu identity. |
| **Navigation Menu List** | Direct child of `Navigation Menu`. | Groups authored top-level menu items. The runtime scans for this list when initializing. |
| **Navigation Menu Item** | Direct child of `Navigation Menu List`. | One navigation entry — a plain link, a dropdown parent, or both. |
| **Navigation Menu Trigger** | Inside `Navigation Menu Item`. | Interactive button that opens a dropdown panel. |
| **Navigation Menu Content** | Inside `Navigation Menu Item`, alongside a trigger. | Dropdown or mega-menu panel content. |
| **Navigation Menu Mobile Item** | Inside `Navigation Menu Content`. | Declares an explicit mobile tree for complex mega-menu content. |
| **Navigation Menu Mobile** | **Separate** from the desktop menu — anywhere on the page, connected by `menuId`. | Mirrors the desktop menu into a drawer-based mobile experience at runtime. |

## Quick Start

<CodeExample title="Basic dropdown menu" language="jsx">
{`<OmeNavigationMenu identity='{{"menuId":"primary-nav"}}'>
  {#slot default}
    <OmeNavigationMenuList>
      {#slot default}
        <OmeNavigationMenuItem>
          {#slot default}
            <OmeNavigationMenuTrigger settings='{{"showArrow":true,"triggerAction":"click"}}'>
              {#slot default}Products{/slot}
            </OmeNavigationMenuTrigger>
            <OmeNavigationMenuContent>
              {#slot default}
                Shop all products
              {/slot}
            </OmeNavigationMenuContent>
          {/slot}
        </OmeNavigationMenuItem>

        <OmeNavigationMenuItem>
          {#slot default}
            About
          {/slot}
        </OmeNavigationMenuItem>
      {/slot}
    </OmeNavigationMenuList>
  {/slot}
</OmeNavigationMenu>`}
</CodeExample>

<CodeExample title="Adding the mobile drawer helper" language="jsx">
{`<!-- Desktop menu -->
<OmeNavigationMenu
  identity='{{"menuId":"primary-nav"}}'
  settings='{{"useAnimatedMenu":true,"useFullWidth":true,"fullWidthTargetSelector":".site-shell"}}'
>
  {#slot default}
    <OmeNavigationMenuList>
      {#slot default}
        <OmeNavigationMenuItem>
          {#slot default}
            <OmeNavigationMenuTrigger settings='{{"showArrow":true,"triggerAction":"click"}}'>
              {#slot default}Services{/slot}
            </OmeNavigationMenuTrigger>
            <OmeNavigationMenuContent>
              {#slot default}
                Design
                Development
              {/slot}
            </OmeNavigationMenuContent>
          {/slot}
        </OmeNavigationMenuItem>
      {/slot}
    </OmeNavigationMenuList>
  {/slot}
</OmeNavigationMenu>

<!-- Mobile helper (separate, linked by menuId) -->
<OmeNavigationMenuMobile
  identity='{{"menuId":"primary-nav"}}'
  content='{{"triggerLabel":"Open menu"}}'
/>`}
</CodeExample>

The mobile helper reads the desktop menu structure at runtime and generates a drawer-based mobile navigation. No duplicate authoring is needed.

---

## Family Components

### Navigation Menu (Root)

The root component wraps all desktop navigation items and manages shared behavior: orientation, keyboard navigation, animated viewport mode, and the menu identity used by the mobile helper. It does not render significant visible UI itself — only a container element and an optional viewport.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="nav">
    HTML tag for the root element. Defaults to `nav` for proper landmark semantics.
  </Prop>
</PropsTable>

<PropsTable group="Identity">
  <Prop name="menuId" type="string" defaultValue='""'>
    Shared menu ID that `Navigation Menu Mobile` uses to find and connect to this desktop menu. Set this to the same value on both components to enable mobile mirroring.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="orientation" type={`"horizontal" | "vertical"`} defaultValue="horizontal">
    Root orientation. Affects which arrow keys navigate between triggers and the layout direction. Use `vertical` for stacked or sidebar menu structures.
  </Prop>
  <Prop name="useAnimatedMenu" type="boolean" defaultValue="false">
    Enables the generated viewport that animates dropdown panel changes. When enabled, content panels are portaled into a viewport container with smooth height transitions and directional motion.
  </Prop>
  <Prop name="useFullWidth" type="boolean" defaultValue="false">
    Makes the animated viewport align to a wider container instead of the trigger width. Requires `useAnimatedMenu` to be enabled and a `fullWidthTargetSelector` to be set.
  </Prop>
  <Prop name="fullWidthTargetSelector" type="string" defaultValue='""'>
    CSS selector used to find the container element whose width the viewport should match. Only visible when `useFullWidth` is enabled. Can be a class selector like `.site-shell`.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-nav-menu-default">
    CSS class applied to the root element.
  </Prop>
  <Prop name="viewportClass" type="class" defaultValue="ome-nav-menu-default__viewport">
    CSS class applied to the generated animated viewport container. Only visible when `useAnimatedMenu` is enabled.
  </Prop>
</PropsTable>

#### Animated Viewport Mode

When `useAnimatedMenu` is `true`, the root component generates a viewport container that houses all dropdown panels. This enables:

- Smooth height transitions as panels open and close.
- Directional motion animations when switching between panels (content slides in from the direction of the newly focused trigger).
- Full-width mode that aligns the viewport to a wider parent container for mega-menu layouts.

Animated mode only activates for **horizontal** orientation and is automatically disabled inside the Etch builder (where it falls back to a non-animated preview).

#### Keyboard Behavior

When a trigger has focus in a **horizontal** menu:

| Key | Action |
|---|---|
| `Enter` / `Space` | Toggle the associated panel. |
| `ArrowRight` | Move focus to the next trigger. |
| `ArrowLeft` | Move focus to the previous trigger. |
| `ArrowDown` | Open the dropdown panel. |
| `Home` | Move focus to the first trigger. |
| `End` | Move focus to the last trigger. |
| `Escape` | Close the open panel and return focus to its trigger. |

In a **vertical** menu, `ArrowDown` / `ArrowUp` navigate between triggers and `ArrowRight` opens the panel.

Navigation wraps by default — pressing `ArrowRight` on the last trigger focuses the first, and vice versa.

#### Accessibility

The runtime wires up ARIA attributes automatically:

- Each trigger has `aria-expanded` and `aria-haspopup="menu"` that update as panels open and close.
- Each content panel has `role="menu"` and syncs `aria-hidden` with its open state.
- `data-ome-state` attributes (`open` / `closed`) are set on items, triggers, and content for CSS targeting.

---

### Navigation Menu List

The list container groups top-level menu items. The runtime scans for the direct `Navigation Menu List` child when initializing the root component.

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-nav-menu-default__list">
    CSS class applied to the list element.
  </Prop>
</PropsTable>

The list always renders as a `` element with `display: flex` and reset list styles. In horizontal mode the flex direction is `row`; in vertical mode it becomes `column`.

---

### Navigation Menu Item

Each `NavigationMenuItem` represents one navigation entry. It can contain a plain link, a trigger-plus-content dropdown pair, or a combination of both (a link that also opens a dropdown).

<PropsTable group="Settings">
  <Prop name="disabled" type="boolean" defaultValue="false">
    Disables this item and any trigger it contains. Disabled items cannot be opened or focused for activation.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-nav-menu-default__item">
    CSS class applied to the item element.
  </Prop>
</PropsTable>

The item always renders as an `` element. The runtime tracks open/closed state via `data-ome-state` on the item.

---

### Navigation Menu Trigger

The interactive button users click or hover to open a dropdown panel. The trigger always renders as a `<button>` element and can optionally display a built-in chevron icon.

<PropsTable group="Settings">
  <Prop name="disabled" type="boolean" defaultValue="false">
    Prevents this trigger from opening its content panel. Overrides the item-level disabled prop.
  </Prop>
  <Prop name="showArrow" type="boolean" defaultValue="false">
    Shows a built-in chevron icon inside the trigger button. The chevron rotates 180 degrees when the panel is open.
  </Prop>
  <Prop name="triggerAction" type={`"hover" | "click"`} defaultValue="hover">
    Preferred interaction mode. `hover` opens the panel on mouse enter with a small delay; `click` requires an explicit click. Inside the Etch builder, all triggers behave as click for reliable editing.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-nav-menu-default__trigger">
    CSS class applied to the trigger button.
  </Prop>
</PropsTable>

#### Trigger Interaction Modes

- **`hover`** — The panel opens when the cursor enters the trigger area (with a 50ms delay) and closes when the cursor leaves the entire navigation boundary (trigger + content + viewport). This is the default and provides the smoothest desktop experience.
- **`click`** — The panel only opens on explicit click. A document-level click listener closes the panel when clicking outside the navigation root.

Both modes support keyboard activation via `Enter` / `Space`.

---

### Navigation Menu Content

The dropdown or mega-menu panel paired with a trigger. Content panels are hidden by default and become visible when their sibling trigger opens them.

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-nav-menu-default__content">
    CSS class applied to the content panel.
  </Prop>
</PropsTable>

#### Runtime Behavior

- In non-animated mode, content is positioned absolutely below its trigger item (or to the right in vertical orientation).
- In animated mode, content is portaled into the shared viewport container and animated with presence state transitions.
- Content panels use `role="menu"`, `aria-hidden`, and `data-ome-state` that sync between `open` and `closed`.
- The `hidden` attribute is toggled to control visibility.

---

### Navigation Menu Mobile

A separate component that mirrors the desktop navigation into a drawer-based mobile experience. It is **not** nested inside `Navigation Menu` — it is placed elsewhere on the page and connects to the desktop menu via a shared `menuId`.

At runtime, the mobile helper:

1. Finds the desktop `Navigation Menu` root by matching `menuId`.
2. Extracts the menu structure recursively (or reads explicit `Navigation Menu Mobile Item` nodes if present).
3. Generates a panel-based mobile drawer with sliding navigation and a back button.

In the Etch builder, the helper shows a static preview instead of live-extracted mobile navigation.

<PropsTable group="Identity">
  <Prop name="menuId" type="string" defaultValue="ome-mobile-nav">
    Must match `Navigation Menu.identity.menuId`. The mobile helper uses this value to find the desktop menu and wire its internal drawer.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="drawerDirection" type={`"left" | "right" | "top" | "bottom"`} defaultValue="left">
    Direction the mobile drawer slides from.
  </Prop>
  <Prop name="dismissible" type="boolean" defaultValue="true">
    Allows overlay click and `Escape` to close the mobile drawer.
  </Prop>
</PropsTable>

<PropsTable group="Content">
  <Prop name="showTriggerLabel" type="boolean" defaultValue="true">
    When enabled, the trigger displays visible label text. When disabled, only a hamburger icon is shown with a separate `triggerAriaLabel` for screen readers.
  </Prop>
  <Prop name="triggerLabel" type="string" defaultValue="Menu">
    Visible label text and accessible name for the drawer trigger. Only shown when `showTriggerLabel` is `true`.
  </Prop>
  <Prop name="triggerAriaLabel" type="string" defaultValue="Menu">
    Screen-reader-only label for the trigger button when `showTriggerLabel` is `false`.
  </Prop>
  <Prop name="backButtonLabel" type="string" defaultValue="Back">
    Fallback label used by the back button when the `backButton` slot is empty.
  </Prop>
</PropsTable>

<PropsTable group="Root Styling">
  <Prop name="class" type="class" defaultValue="ome-mobile-nav-default">
    CSS class applied to the mobile helper wrapper.
  </Prop>
</PropsTable>

<PropsTable group="Control Styling">
  <Prop name="triggerClass" type="class" defaultValue="ome-mobile-nav-default__trigger">
    CSS class applied to the drawer trigger button.
  </Prop>
  <Prop name="backButtonClass" type="class" defaultValue="ome-mobile-nav-default__back">
    CSS class applied to the back button.
  </Prop>
  <Prop name="closeButtonClass" type="class" defaultValue="ome-mobile-nav-default__close">
    CSS class applied to the icon-only close button.
  </Prop>
</PropsTable>

<PropsTable group="Drawer Styling">
  <Prop name="drawerClass" type="class" defaultValue="ome-mobile-nav-default__drawer">
    CSS class applied to the internal drawer root.
  </Prop>
  <Prop name="headerClass" type="class" defaultValue="ome-mobile-nav-default__header">
    CSS class applied to the drawer header row.
  </Prop>
  <Prop name="contentClass" type="class" defaultValue="ome-mobile-nav-default__content">
    CSS class applied to the drawer content wrapper.
  </Prop>
</PropsTable>

<PropsTable group="Menu Styling">
  <Prop name="listClass" type="class" defaultValue="ome-mobile-nav-default__list">
    CSS class applied to generated root and child lists.
  </Prop>
  <Prop name="itemClass" type="class" defaultValue="ome-mobile-nav-default__item">
    CSS class applied to generated list items.
  </Prop>
  <Prop name="linkClass" type="class" defaultValue="ome-mobile-nav-default__link">
    CSS class applied to generated links.
  </Prop>
  <Prop name="submenuTriggerClass" type="class" defaultValue="ome-mobile-nav-default__submenu-trigger">
    CSS class applied to generated submenu trigger buttons.
  </Prop>
  <Prop name="nextIconClass" type="class" defaultValue="ome-mobile-nav-default__next-icon">
    CSS class applied to the chevron icon shown on generated submenu triggers.
  </Prop>
</PropsTable>

<PropsTable group="Slot Styling">
  <Prop name="beforeClass" type="class" defaultValue="ome-mobile-nav-default__before">
    CSS class applied to the wrapper around the `before` slot.
  </Prop>
  <Prop name="afterClass" type="class" defaultValue="ome-mobile-nav-default__after">
    CSS class applied to the wrapper around the `after` slot.
  </Prop>
</PropsTable>

#### Slots

| Slot | Description |
|---|---|
| `before` | Renders before generated items in the root panel only. |
| `after` | Renders after generated items in the root panel only. |
| `backButton` | Replaces the default back button content. If omitted, the button falls back to a back icon plus `backButtonLabel`. |

#### Mobile Keyboard Behavior

Inside the mobile drawer, when a menu item has focus:

| Key | Action |
|---|---|
| `ArrowDown` | Move focus to the next menu item. |
| `ArrowUp` | Move focus to the previous menu item. |
| `Home` | Move focus to the first menu item. |
| `End` | Move focus to the last menu item. |
| `Enter` / `Space` | Activate the focused item (follow link or open submenu). |
| `ArrowRight` | Open a submenu (when focused on a submenu trigger). |
| `Escape` / `ArrowLeft` | Go back to the previous panel. |

#### How Content Extraction Works

The mobile helper extracts the navigation tree from the desktop menu in one of two ways:

1. **Explicit mobile items** — If a `Navigation Menu Content` subtree contains at least one `Navigation Menu Mobile Item`, the helper uses only those explicit nodes for that panel and ignores regular auto-generated links.
2. **Automatic extraction** — If no explicit mobile items exist, the helper recursively extracts the authored structure: links become mobile links, items with triggers become submenu branches, and nested navigation roots are traversed.

---

### Navigation Menu Mobile Item

An explicit mobile-only node authored inside `Navigation Menu Content` when complex mega-menu content needs a custom mobile tree that differs from the auto-extracted structure. If a content panel contains at least one `Navigation Menu Mobile Item`, the mobile helper uses only those explicit nodes.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="div">
    HTML tag for the authored wrapper element. Common values are `div`, `ul`, `li`, or `a` depending on the structural role.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="useAs" type={`"trigger" | "link"`} defaultValue="trigger">
    Mobile node mode. `trigger` creates a submenu branch that navigates to a child panel. `link` creates a direct navigation link.
  </Prop>
</PropsTable>

<PropsTable group="Content">
  <Prop name="label" type="string" defaultValue='""'>
    Label shown in the generated mobile drawer UI for this node.
  </Prop>
</PropsTable>

<PropsTable group="Link">
  <Prop name="href" type="string" defaultValue='""'>
    Destination URL. Required when `useAs` is `link`. Ignored for `trigger` nodes.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="[]">
    Optional CSS class list for the authored wrapper element.
  </Prop>
</PropsTable>

#### When to Use Mobile Items

Use `Navigation Menu Mobile Item` when your desktop mega-menu has complex content (cards, images, multi-column layouts) that should not be naively extracted into the mobile drawer. By wrapping mobile-relevant content in explicit mobile items, you control exactly what appears in the mobile navigation while keeping the desktop layout free to include richer visual content.

---

## Common Mistakes

<CommonMistake title="Placing items directly inside Navigation Menu">
  Do not place `Navigation Menu Item` blocks directly inside `Navigation Menu`. The runtime scans for a direct `Navigation Menu List` child at initialization. Always nest items inside a list.
</CommonMistake>

<CommonMistake title="Nesting Navigation Menu Mobile inside Navigation Menu">
  `Navigation Menu Mobile` is a **separate** component. It must not be placed inside the desktop `Navigation Menu`. Place it elsewhere on the page and connect it via `menuId`.
</CommonMistake>

<CommonMistake title="Missing trigger for dropdown content">
  If an item has `Navigation Menu Content`, it must also have a sibling `Navigation Menu Trigger`. Content without a trigger will not be openable.
</CommonMistake>

<CommonMistake title="Mismatched menuId values">
  The `menuId` on `Navigation Menu Mobile.identity.menuId` must exactly match `Navigation Menu.identity.menuId`. If the IDs do not match, the mobile helper will not find the desktop menu and the drawer will be empty.
</CommonMistake>

<CommonMistake title="Duplicating mobile content manually">
  Do not author a separate second menu tree for mobile. `Navigation Menu Mobile` derives its structure from the desktop menu automatically. Only use `Navigation Menu Mobile Item` when you need to customize the mobile tree for complex content.
</CommonMistake>

---

## FAQs

<details>
<summary>Can I use Navigation Menu without the mobile helper?</summary>

Yes. `Navigation Menu Mobile` is entirely optional. The desktop navigation family works standalone. Only add the mobile helper when you need a drawer-based mobile experience that mirrors the desktop menu.

</details>

<details>
<summary>How does the animated viewport work?</summary>

When `useAnimatedMenu` is enabled on the root component, dropdown content panels are portaled into a shared viewport container. The viewport smoothly transitions its height when panels open and close, and content panels animate with directional motion (slide in from the direction of the newly focused trigger). For full-width mega-menus, enable `useFullWidth` and set `fullWidthTargetSelector` to the container the viewport should align to.

</details>

<details>
<summary>What happens when both a link and a trigger are in the same item?</summary>

The item renders as a "link-branch" — the link is clickable for navigation and the trigger opens a dropdown. In the mobile drawer, this becomes a row with both a tappable link and a submenu chevron button.

</details>

<details>
<summary>Can I nest navigation menus?</summary>

Yes. You can place a `Navigation Menu` inside a `Navigation Menu Content` panel for multi-level navigation. Nested menus manage their own state independently. The animated viewport is automatically disabled for nested menus to avoid conflicts.

</details>

<details>
<summary>How do I customize the mobile drawer trigger?</summary>

Use `showTriggerLabel` to toggle between a labeled trigger and an icon-only trigger. When `showTriggerLabel` is `false`, set `triggerAriaLabel` for screen reader accessibility. Style the trigger using `controlStyling.triggerClass`. For the hamburger icon itself, the component renders a built-in SVG.

</details>

<details>
<summary>When should I use Navigation Menu Mobile Item?</summary>

Use it when your desktop mega-menu has complex visual content (cards, multi-column layouts, featured sections) that should not be auto-extracted into the mobile drawer. By wrapping mobile-relevant content in explicit `Navigation Menu Mobile Item` nodes, you control exactly what appears in the mobile navigation. Simple dropdown menus do not need explicit mobile items — the auto-extraction handles them well.

</details>
