Skip to main content

Navigation Menu

Navigate a website through links and animated shared panels.

Use NavigationMenu for website destinations, with direct links and expandable link panels. Use DropdownMenu for a menu of actions.

Basic usage#

import { NavigationMenu } from 'moraine'

export function Example() {
  return (
    <NavigationMenu aria-label="Main navigation">
      <NavigationMenu.List>
        <NavigationMenu.Item value="products">
          <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
          <NavigationMenu.Content>
            <NavigationMenu.Link href="/overview" closeOnClick>
              Overview
            </NavigationMenu.Link>
          </NavigationMenu.Content>
        </NavigationMenu.Item>
        <NavigationMenu.Item>
          <NavigationMenu.Link href="/docs">Documentation</NavigationMenu.Link>
        </NavigationMenu.Item>
      </NavigationMenu.List>
    </NavigationMenu>
  )
}

Playground#

Props
Slots

Anatomy#

NavigationMenu [component; slot=root]
├── NavigationMenu.List [part; slot=list]
│   └── NavigationMenu.Item [part; slot=item]
│       └── NavigationMenu.Trigger [part; slot=trigger]
│           └── triggerIcon [slot]
└── NavigationMenu.Content [part; slot=content]
    └── NavigationMenu.Link [part; slot=link]

Usage#

Basic#

Combine expandable panels with direct links. Use NavigationMenu.Content for each panel and NavigationMenu.Link for its destinations.

import { NavigationMenu } from 'moraine'

export function Basic() {
  return (
    <div class="pt-4 flex min-h-64 w-full justify-center">
      <NavigationMenu aria-label="Example navigation">
        <NavigationMenu.List>
          <NavigationMenu.Item value="products">
            <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="gap-2 grid w-[min(28rem,calc(100vw-2rem))] sm:grid-cols-2">
                <NavigationMenu.Link href="#overview" closeOnClick>
                  <span class="font-medium">Overview</span>
                  <span class="text-muted-foreground">Explore the component library.</span>
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#themes" closeOnClick>
                  <span class="font-medium">Themes</span>
                  <span class="text-muted-foreground">Make the library fit your product.</span>
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#accessibility" closeOnClick>
                  <span class="font-medium">Accessibility</span>
                  <span class="text-muted-foreground">Keyboard and screen reader support.</span>
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#getting-started" closeOnClick>
                  <span class="font-medium">Getting started</span>
                  <span class="text-muted-foreground">Install your first component.</span>
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
          <NavigationMenu.Item value="guides">
            <NavigationMenu.Trigger>Guides</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="w-48">
                <NavigationMenu.Link href="#installation" closeOnClick>
                  Installation
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#composition" closeOnClick>
                  Composition
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#styling" closeOnClick>
                  Styling
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
          <NavigationMenu.Item>
            <NavigationMenu.Link href="#documentation" active>
              Documentation
            </NavigationMenu.Link>
          </NavigationMenu.Item>
        </NavigationMenu.List>
      </NavigationMenu>
    </div>
  )
}

Controlled state and disabled items#

Use stable item values with value and onValueChange. Set the value to null to close. Disabled or removed items cannot keep a panel open. defaultValue opens an item after hydration.

import { Button, NavigationMenu } from 'moraine'
import { createSignal } from 'solid-js'

export function Controlled() {
  const [value, setValue] = createSignal<string | null>(null)
  const [disabled, setDisabled] = createSignal(false)
  return (
    <div class="min-h-56 space-y-4">
      <div class="flex flex-wrap gap-2">
        <Button size="sm" variant="outline" onClick={() => setValue('account')}>
          Open account
        </Button>
        <Button size="sm" variant="outline" onClick={() => setValue(null)}>
          Close
        </Button>
        <Button size="sm" variant="outline" onClick={() => setDisabled((value) => !value)}>
          Toggle disabled
        </Button>
      </div>
      <NavigationMenu
        aria-label="Account navigation"
        value={value()}
        onValueChange={setValue}
        disabled={disabled()}
      >
        <NavigationMenu.List>
          <NavigationMenu.Item value="account">
            <NavigationMenu.Trigger>Account</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="w-48">
                <NavigationMenu.Link href="#profile" closeOnClick>
                  Profile
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#settings" closeOnClick>
                  Settings
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
          <NavigationMenu.Item value="billing" disabled>
            <NavigationMenu.Trigger>Billing</NavigationMenu.Trigger>
            <NavigationMenu.Content>Billing is unavailable.</NavigationMenu.Content>
          </NavigationMenu.Item>
        </NavigationMenu.List>
      </NavigationMenu>
      <p class="text-xs text-muted-foreground">Open item: {value() ?? 'none'}</p>
    </div>
  )
}

Orientation#

Set orientation="vertical" for a vertical list. Use placement="right" to place its panels beside the triggers.

import { NavigationMenu } from 'moraine'

export function Orientation() {
  return (
    <div class="min-h-56 w-full">
      <NavigationMenu aria-label="Vertical navigation" orientation="vertical" placement="right">
        <NavigationMenu.List>
          <NavigationMenu.Item value="products">
            <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="w-40">
                <NavigationMenu.Link href="#overview" closeOnClick>
                  Overview
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#features" closeOnClick>
                  Features
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
          <NavigationMenu.Item value="resources">
            <NavigationMenu.Trigger>Resources</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="w-40">
                <NavigationMenu.Link href="#guides" closeOnClick>
                  Guides
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#support" closeOnClick>
                  Support
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
        </NavigationMenu.List>
      </NavigationMenu>
    </div>
  )
}

RTL#

Set dir="rtl" on the surrounding container for right-to-left navigation.

import { NavigationMenu } from 'moraine'

export function Rtl() {
  return (
    <div dir="rtl" class="min-h-56 w-full">
      <NavigationMenu aria-label="RTL navigation">
        <NavigationMenu.List>
          <NavigationMenu.Item value="products">
            <NavigationMenu.Trigger>المنتجات</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <div class="w-40">
                <NavigationMenu.Link href="#overview" closeOnClick>
                  نظرة عامة
                </NavigationMenu.Link>
                <NavigationMenu.Link href="#features" closeOnClick>
                  الميزات
                </NavigationMenu.Link>
              </div>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
          <NavigationMenu.Item value="resources">
            <NavigationMenu.Trigger>الموارد</NavigationMenu.Trigger>
            <NavigationMenu.Content>
              <NavigationMenu.Link href="#guides" closeOnClick>
                الأدلة
              </NavigationMenu.Link>
            </NavigationMenu.Content>
          </NavigationMenu.Item>
        </NavigationMenu.List>
      </NavigationMenu>
    </div>
  )
}

Use linkRender for a router’s link component. Forward the supplied attributes, events, children, object style and ref to the anchor. active marks the current page with aria-current="page". closeOnClick defaults to false; enable it when activation should also close the panel. A cancelled click does not request closing.

import { A } from '@solidjs/router'
import { NavigationMenu } from 'moraine'
import type { NavigationMenuT } from 'moraine'

const RouterLink = (props: NavigationMenuT.LinkRenderProps) => (
  <A {...props} href={props.href ?? '#'} />
)

export function RouterLinkExample() {
  return (
    <NavigationMenu aria-label="Documentation navigation">
      <NavigationMenu.List>
        <NavigationMenu.Item>
          <NavigationMenu.Link linkRender={RouterLink} href="/components/button">
            Button
          </NavigationMenu.Link>
        </NavigationMenu.Item>
        <NavigationMenu.Item>
          <NavigationMenu.Link linkRender={RouterLink} href="/components/tabs">
            Tabs
          </NavigationMenu.Link>
        </NavigationMenu.Item>
      </NavigationMenu.List>
    </NavigationMenu>
  )
}

Keyboard interaction#

Arrow keys along the list axis move focus without opening a panel. Home and End focus the first and last enabled navigation controls. Enter and Space activate a trigger. ArrowDown opens a horizontal menu and enters its content; ArrowRight does the same for a vertical LTR menu, and ArrowLeft for RTL.

Tab and Shift+Tab preserve the order between the trigger, its portaled content and the following navigation control. Arrow keys and Home/End move between panel links, while inputs retain their editing keys. Escape closes the panel and returns focus to its trigger. Outside click and focus close the panel without taking focus away from the destination.

The root renders a navigation landmark. Give it an accessible name when the page has several navigation landmarks. This version supports one level of panels.

Attributes#

Attributes
data-disabledSlot: navigation-menu, navigation-menu-item, navigation-menu-trigger, navigation-menu-linkDescription: Present when the component, slot, or item is disabled.
data-orientationSlot: navigation-menu, navigation-menu-list, navigation-menu-contentDescription: Stores the rendered orientation (horizontal or vertical).
data-expandedSlot: navigation-menu-item, navigation-menu-trigger, navigation-menu-trigger-icon, navigation-menu-contentDescription: Present when the panel, accordion, or menu is expanded.
data-closedSlot: navigation-menu-contentDescription: Present when disclosure or transition content is closed.
data-activeSlot: navigation-menu-linkDescription: Present when the item or trigger is active.

Props#

NavigationMenu#

Renders a <nav> element by default.

Prop

List#

Renders a <ul> element by default.

Prop

Item#

Renders a <li> element by default.

Prop

Trigger#

Renders a <button> element by default.

Prop

Content#

Renders a <div> element by default.

Prop

Renders a <a> element by default.

Prop