NavigationMenu

Composes navigation links and dropdown content panels.

<script setup lang="ts">
  import { NuxtLink } from '#components'
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
  } from '@hina-ui/vue'

  const localePath = useLocalePath()
</script>

<template>
  <Stack class="min-h-96 w-full sm:min-h-64" align="start">
    <NavigationMenu label="Main navigation">
      <NavigationMenuItem value="start">
        <NavigationMenuTrigger>Start</NavigationMenuTrigger>
        <NavigationMenuContent>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/guide/installation')"
            description="Install and configure the library"
          >
            Installation
          </NavigationMenuLink>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/design/colors')"
            description="Color roles and theme variables"
          >
            Colors
          </NavigationMenuLink>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/design/motion')"
            description="Animation duration and easing"
          >
            Motion
          </NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem value="components">
        <NavigationMenuTrigger>Components</NavigationMenuTrigger>
        <NavigationMenuContent class="grid w-[30rem] gap-1 sm:grid-cols-2">
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/components/button')"
            description="Buttons and interaction feedback"
          >
            Button
          </NavigationMenuLink>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/components/dialog')"
            description="Dialogs and custom content"
          >
            Dialog
          </NavigationMenuLink>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/components/form')"
            description="Form state and async validation"
          >
            Form
          </NavigationMenuLink>
          <NavigationMenuLink
            :as="NuxtLink"
            :to="localePath('/components/data-table')"
            description="Data display and column interactions"
          >
            DataTable
          </NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink :as="NuxtLink" :to="localePath('/changelog')">
          Changelog
        </NavigationMenuLink>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

Usage

import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuTrigger,
  NavigationMenuContent,
  NavigationMenuLink,
} from '@hina-ui/vue'
ts

NavigationMenu provides the navigation landmark, list, and shared content viewport. Wrap each top-level entry in NavigationMenuItem, containing either a NavigationMenuLink or a pair of NavigationMenuTrigger and NavigationMenuContent. Name the navigation with label or aria-labelledby.

active marks the current page and sets aria-current="page". The current page and expanded item are independent states.

<script setup lang="ts">
  import { NavigationMenu, NavigationMenuItem, NavigationMenuLink } from '@hina-ui/vue'
</script>

<template>
  <NavigationMenu label="Page navigation">
    <NavigationMenuItem>
      <NavigationMenuLink href="#usage" active>Usage</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="#examples">Examples</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="#api">API</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenu>
</template>
vue

Examples

Click and controlled state

Mouse hover and click both open panels by default. trigger="click" disables hover activation and keeps the panel open when the pointer leaves. v-model binds the expanded item’s value; an empty string closes all panels. Give panel items stable, unique values when controlling expansion.

Selecting a link closes the panel. @select.prevent keeps it open without preventing link navigation. Use @click.prevent to prevent navigation itself.

Open item: None

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const value = ref('')
</script>

<template>
  <Stack class="min-h-56 w-full" align="start">
    <Text size="sm" tone="muted">Open item: {{ value || 'None' }}</Text>
    <NavigationMenu v-model="value" label="Click to open" trigger="click">
      <NavigationMenuItem value="links">
        <NavigationMenuTrigger>Links</NavigationMenuTrigger>
        <NavigationMenuContent>
          <NavigationMenuLink href="#controlled">Close after selection</NavigationMenuLink>
          <NavigationMenuLink href="#controlled" @select.prevent>
            Stay open after selection
          </NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

Custom content

NavigationMenuContent accepts arbitrary content through its default slot. Set its width and layout with class, and remove the default padding with :padded="false".

NavigationMenuLink provides icon, description, and trailing slots. This example combines Text, Divider, and Tag. Use NavigationMenuLink for links inside panels as well, preserving keyboard navigation and dismissal after selection.

<script setup lang="ts">
  import { NuxtLink } from '#components'
  import { BookOpen, ArrowUpRight, Palette } from '@lucide/vue'
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
    Text,
    Tag,
    Divider,
  } from '@hina-ui/vue'

  const localePath = useLocalePath()
</script>

<template>
  <Stack class="min-h-72 w-full" align="start">
    <NavigationMenu label="Custom navigation">
      <NavigationMenuItem value="resources">
        <NavigationMenuTrigger>
          <template #icon><BookOpen /></template>
          Resources
        </NavigationMenuTrigger>
        <NavigationMenuContent :padded="false" class="w-96">
          <Stack gap="xs" class="p-4">
            <Text weight="medium">Design and components</Text>
            <Text size="sm" tone="muted">Compose the content through its slot.</Text>
          </Stack>
          <Divider />
          <Stack gap="xs" class="p-2">
            <NavigationMenuLink
              :as="NuxtLink"
              :to="localePath('/design/colors')"
              description="Semantic colors, palettes and themes"
            >
              <template #icon><Palette /></template>
              Colors
              <template #trailing><Tag size="sm">Updated</Tag></template>
            </NavigationMenuLink>
            <NavigationMenuLink
              href="https://github.com/Hikarinagi/ui"
              target="_blank"
              rel="noopener noreferrer"
            >
              GitHub
              <template #trailing><ArrowUpRight /></template>
            </NavigationMenuLink>
          </Stack>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

Vertical

orientation="vertical" arranges entries vertically and opens panels toward the inline end. Panels use the opposite side and limit their width when space is insufficient. align controls alignment with the trigger.

<script setup lang="ts">
  import { NuxtLink } from '#components'
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
  } from '@hina-ui/vue'

  const localePath = useLocalePath()
</script>

<template>
  <Stack class="min-h-56 w-full" align="start">
    <NavigationMenu label="Vertical navigation" orientation="vertical" align="start" size="sm">
      <NavigationMenuItem value="design">
        <NavigationMenuTrigger>Design</NavigationMenuTrigger>
        <NavigationMenuContent class="w-64">
          <NavigationMenuLink :as="NuxtLink" :to="localePath('/design/colors')">
            Colors
          </NavigationMenuLink>
          <NavigationMenuLink :as="NuxtLink" :to="localePath('/design/typography')">
            Typography
          </NavigationMenuLink>
          <NavigationMenuLink :as="NuxtLink" :to="localePath('/design/layout')">
            Layout
          </NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink :as="NuxtLink" :to="localePath('/components')">
          Components
        </NavigationMenuLink>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink :as="NuxtLink" :to="localePath('/changelog')">
          Changelog
        </NavigationMenuLink>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

Sizes

size sets the size of links, triggers, and icons together. It supports sm, md, and lg, and follows the global density. Icons measure 14, 16, and 18px respectively.

<script setup lang="ts">
  import { BookOpen, Code, ArrowUpRight } from '@lucide/vue'
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
  } from '@hina-ui/vue'
</script>

<template>
  <Stack gap="lg" align="start" class="min-h-80 w-full">
    <NavigationMenu
      v-for="size in ['sm', 'md', 'lg'] as const"
      :key="size"
      :label="`${size} navigation`"
      :size="size"
    >
      <NavigationMenuItem value="docs">
        <NavigationMenuTrigger>
          <template #icon><BookOpen /></template>
          {{ size }}
        </NavigationMenuTrigger>
        <NavigationMenuContent class="w-64">
          <NavigationMenuLink href="#usage">
            <template #icon><BookOpen /></template>
            Usage
          </NavigationMenuLink>
          <NavigationMenuLink href="#api">
            <template #icon><Code /></template>
            API
            <template #trailing><ArrowUpRight /></template>
          </NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink href="#usage" active>Usage</NavigationMenuLink>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink href="#api">API</NavigationMenuLink>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

States

Both triggers and links support disabled. Disabled entries cannot activate and are skipped by arrow navigation.

<script setup lang="ts">
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuLink,
  } from '@hina-ui/vue'
</script>

<template>
  <NavigationMenu label="Navigation states" size="sm">
    <NavigationMenuItem>
      <NavigationMenuLink href="#states" active>Current</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="#api">Link</NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuTrigger disabled>Disabled panel</NavigationMenuTrigger>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="#states" disabled>Disabled link</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenu>
</template>
vue

Router links

NavigationMenuLink with as-child forwards attributes and behavior to its single child, including RouterLink or NuxtLink. Alternatively, pass a component through as. Set active from your route state.

<script setup lang="ts">
  import { NavigationMenu, NavigationMenuItem, NavigationMenuLink } from '@hina-ui/vue'

  const localePath = useLocalePath()
</script>

<template>
  <NavigationMenu label="Router navigation">
    <NavigationMenuItem>
      <NavigationMenuLink as-child active>
        <NuxtLink :to="localePath('/components/navigation-menu')">NavigationMenu</NuxtLink>
      </NavigationMenuLink>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink as-child>
        <NuxtLink :to="localePath('/components/toolbar')">Toolbar</NuxtLink>
      </NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenu>
</template>
vue

RTL

Direction inherits from an ancestor’s dir or Reka’s global configuration, or can be set explicitly with dir="rtl". Layout, alignment, disclosure arrows, and keyboard direction follow it.

<script setup lang="ts">
  import {
    NavigationMenu,
    NavigationMenuItem,
    NavigationMenuTrigger,
    NavigationMenuContent,
    NavigationMenuLink,
    Stack,
  } from '@hina-ui/vue'
</script>

<template>
  <Stack class="min-h-56 w-full" dir="rtl" align="start">
    <NavigationMenu label="RTL navigation" align="start">
      <NavigationMenuItem value="first">
        <NavigationMenuTrigger>First</NavigationMenuTrigger>
        <NavigationMenuContent class="w-64">
          <NavigationMenuLink href="#rtl">Link one</NavigationMenuLink>
          <NavigationMenuLink href="#rtl">Link two</NavigationMenuLink>
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink href="#usage">Second</NavigationMenuLink>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink href="#api">Third</NavigationMenuLink>
      </NavigationMenuItem>
    </NavigationMenu>
  </Stack>
</template>
vue

Behavior

  • One panel is expanded at a time. The shared viewport transitions between content sizes, while panels transition in the switching direction.
  • Content renders inside the navigation element, above the normal document flow. Ancestors need visible overflow so they do not clip the panels.
  • Panel widths and horizontal positions account for the browser and scrolling or clipping ancestors. Allow vertical space for expanded content. Compose ScrollArea inside a panel for long content.
  • Hover menus close after leaving both the navigation and its content. Exiting panels stop receiving pointer events.
  • Outside pointer interaction, focus leaving the navigation, and Esc dismiss the panel. Set :unmount-on-hide="false" to preserve hidden component state.
  • The component uses nav, ul, li, and link semantics. For menu commands, use Menubar or DropdownMenu.

Accessibility

KeyBehavior
Tab / Shift+TabMove through navigation and expanded content
Enter / SpaceToggle the focused trigger
Horizontal ← / →, vertical ↑ / ↓Move between top-level entries, skipping disabled items; horizontal direction reverses in RTL
Home / EndFocus the first or last available top-level entry
Horizontal ↓, vertical → (← in RTL)Enter content from an expanded trigger
EscClose content and restore focus to its trigger

API

NavigationMenu

Prop
Type
Default
Description
modelValue
string
''
Expanded item; empty string closes all panels
label
string
—
Accessible navigation name
orientation
'horizontal' | 'vertical'
'horizontal'
Layout direction
dir
'ltr' | 'rtl'
Inherited
Reading direction
size
'sm' | 'md' | 'lg'
'md'
Control size
trigger
'hover' | 'click'
'hover'
Hover and click, or click only
delayDuration
number
200
Initial hover opening delay, in milliseconds
skipDelayDuration
number
300
Window after closing during which re-entry skips the initial delay, in milliseconds
align
'start' | 'center' | 'end'
'center'
Panel alignment with the trigger
unmountOnHide
boolean
true
Unmount hidden content
class
string
—
Navigation root classes
listClass
string
—
List classes
viewportClass
string
—
Shared content viewport classes

The update:modelValue(value: string) event returns the expanded item. The default slot receives { value: string }.

NavigationMenuItem

value?: string identifies the item in the expanded state and is generated when omitted. The default slot contains a link or a trigger and content pair.

NavigationMenuTrigger

Prop
Type
Default
Description
disabled
boolean
false
Disable the trigger
class
string
—
Trigger classes

The default slot provides the label, icon provides a leading icon, and trailing can replace the default DisclosureIcon. The arrow rotates with the open state and points toward inline end in vertical layouts. Renders with type="button" to avoid submitting forms.

NavigationMenuContent

Prop
Type
Default
Description
padded
boolean
true
Apply default padding
class
string
—
Content classes, including width and layout

The default slot provides the content. Forwards escapeKeyDown, pointerDownOutside, focusOutside, interactOutside, and dismiss events. Call preventDefault() on the corresponding interaction event to prevent dismissal.