Skip to content

Navigation Menu

A collection of links for navigating websites.
vue
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
import {
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
import NavigationMenuListItem from './NavigationMenuListItem.vue'

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

<template>
  <NavigationMenuRoot v-model="currentTrigger" class="relative z-[1] flex w-full justify-center">
    <NavigationMenuList class="center shadow-blackA7 m-0 flex list-none rounded-[6px] bg-white p-1 shadow-[0_2px_10px]">
      <NavigationMenuItem>
        <NavigationMenuTrigger
          class="text-grass11 hover:bg-green3 focus:shadow-green7 group flex select-none items-center justify-between gap-[2px] rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none outline-none focus:shadow-[0_0_0_2px]"
        >
          Learn
          <Icon
            icon="radix-icons:caret-down"
            class="text-green10 relative top-[1px] transition-transform duration-[250] ease-in group-data-[state=open]:-rotate-180"
            aria-hidden
          />
        </NavigationMenuTrigger>
        <NavigationMenuContent
          class="data-[motion=from-start]:animate-enterFromLeft data-[motion=from-end]:animate-enterFromRight data-[motion=to-start]:animate-exitToLeft data-[motion=to-end]:animate-exitToRight absolute top-0 left-0 w-full sm:w-auto"
        >
          <ul class="one m-0 grid list-none gap-x-[10px] p-[22px] sm:w-[500px] sm:grid-cols-[0.75fr_1fr]">
            <li class="row-span-3 grid">
              <NavigationMenuLink as-child>
                <a
                  class="focus:shadow-green7 from-green9 to-teal9 flex h-full w-full select-none flex-col justify-end rounded-[6px] bg-gradient-to-b p-[25px] no-underline outline-none focus:shadow-[0_0_0_2px]"
                  href="/"
                >
                  <img class="w-16" src="https://www.radix-vue.com/logo.svg">
                  <div class="mt-4 mb-[7px] text-[18px] font-medium leading-[1.2] text-white">Radix Primitives</div>
                  <p class="text-mauve4 text-[14px] leading-[1.3]">Unstyled, accessible components for Vue.</p>
                </a>
              </NavigationMenuLink>
            </li>

            <NavigationMenuListItem href="https://stitches.dev/" title="Stitches">
              CSS-in-JS with best-in-class developer experience.
            </NavigationMenuListItem>
            <NavigationMenuListItem href="/colors" title="Colors">
              Beautiful, thought-out palettes with auto dark mode.
            </NavigationMenuListItem>
            <NavigationMenuListItem href="https://icons.radix-ui.com/" title="Icons">
              A crisp set of 15x15 icons, balanced and consistent.
            </NavigationMenuListItem>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>

      <NavigationMenuItem>
        <NavigationMenuTrigger
          class="text-grass11 hover:bg-green3 focus:shadow-green7 group flex select-none items-center justify-between gap-[2px] rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none outline-none focus:shadow-[0_0_0_2px]"
        >
          Overview
          <Icon
            icon="radix-icons:caret-down"
            class="text-green10 relative top-[1px] transition-transform duration-[250] ease-in group-data-[state=open]:-rotate-180"
            aria-hidden
          />
        </NavigationMenuTrigger>
        <NavigationMenuContent class="data-[motion=from-start]:animate-enterFromLeft data-[motion=from-end]:animate-enterFromRight data-[motion=to-start]:animate-exitToLeft data-[motion=to-end]:animate-exitToRight absolute top-0 left-0 w-full sm:w-auto">
          <ul class="m-0 grid list-none gap-x-[10px] p-[22px] sm:w-[600px] sm:grid-flow-col sm:grid-rows-3">
            <NavigationMenuListItem title="Introduction" href="/docs/primitives/overview/introduction">
              Build high-quality, accessible design systems and web apps.
            </NavigationMenuListItem>
            <NavigationMenuListItem title="Getting started" href="/docs/primitives/overview/getting-started">
              A quick tutorial to get you up and running with Radix Primitives.
            </NavigationMenuListItem>
            <NavigationMenuListItem title="Styling" href="/docs/primitives/guides/styling">
              Unstyled and compatible with any styling solution.
            </NavigationMenuListItem>
            <NavigationMenuListItem title="Animation" href="/docs/primitives/guides/animation">
              Use CSS keyframes or any animation library of your choice.
            </NavigationMenuListItem>
            <NavigationMenuListItem title="Accessibility" href="/docs/primitives/overview/accessibility">
              Tested in a range of browsers and assistive technologies.
            </NavigationMenuListItem>
            <NavigationMenuListItem title="Releases" href="/docs/primitives/overview/releases">
              Radix Primitives releases and their changelogs.
            </NavigationMenuListItem>
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>

      <NavigationMenuItem>
        <NavigationMenuLink
          class="text-grass11 hover:bg-green3 focus:shadow-green7 block select-none rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none no-underline outline-none focus:shadow-[0_0_0_2px]"
          href="https://github.com/radix-vue"
        >
          Github
        </NavigationMenuLink>
      </NavigationMenuItem>

      <NavigationMenuIndicator
        class="data-[state=hidden]:opacity-0 duration-200 data-[state=visible]:animate-fadeIn data-[state=hidden]:animate-fadeOut top-full z-[1] flex h-[10px] items-end justify-center overflow-hidden transition-[all,transform_250ms_ease]"
      >
        <div class="relative top-[70%] h-[10px] w-[10px] rotate-[45deg] rounded-tl-[2px] bg-white" />
      </NavigationMenuIndicator>
    </NavigationMenuList>

    <div class="perspective-[2000px] absolute top-full left-0 flex w-full justify-center">
      <NavigationMenuViewport
        class="data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut relative mt-[10px] h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[10px] bg-white transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]"
      />
    </div>
  </NavigationMenuRoot>
</template>

Features

  • Can be controlled or uncontrolled.
  • Flexible layout structure with managed tab focus.
  • Supports submenus.
  • Optional active item indicator.
  • Full keyboard navigation.
  • Exposes CSS variables for advanced animation.
  • Supports custom timings.

Installation

Install the component from your command line.

bash
npm install radix-vue

Anatomy

Import all parts and piece them together.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuSub,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger />
        <NavigationMenuContent>
          <NavigationMenuLink />
        </NavigationMenuContent>
      </NavigationMenuItem>

      <NavigationMenuItem>
        <NavigationMenuLink />
      </NavigationMenuItem>

      <NavigationMenuItem>
        <NavigationMenuTrigger />
        <NavigationMenuContent>
          <NavigationMenuSub>
            <NavigationMenuList />
            <NavigationMenuViewport />
          </NavigationMenuSub>
        </NavigationMenuContent>
      </NavigationMenuItem>

      <NavigationMenuIndicator />
    </NavigationMenuList>

    <NavigationMenuViewport />
  </NavigationMenuRoot>
</template>

API Reference

Root

Contains all the parts of a navigation menu.

PropDefaultType
as
'nav'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

defaultValue
string

The value of the menu item that should be active when initially rendered.

Use when you do not need to control the value state.

delayDuration
200
number

The duration from when the pointer enters the trigger until the tooltip gets opened.

dir
'ltr' | 'rtl'

The reading direction of the combobox when applicable.

If omitted, inherits globally from DirectionProvider or assumes LTR (left-to-right) reading mode.

modelValue
string

The controlled value of the menu item to activate. Can be used as v-model.

orientation
'horizontal'
'vertical' | 'horizontal'

The orientation of the menu.

skipDelayDuration
300
number

How much time a user has to enter another trigger without incurring a delay again.

EmitPayload
update:modelValue
[value: string]

Event handler called when the value changes.

Data AttributeValue
[data-orientation]"vertical" | "horizontal"

Sub

Signifies a submenu. Use it in place of the root part when nested to create a submenu.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

defaultValue
string

The value of the menu item that should be active when initially rendered.

Use when you do not need to control the value state.

modelValue
string

The controlled value of the sub menu item to activate. Can be used as v-model.

orientation
'horizontal'
'vertical' | 'horizontal'

The orientation of the menu.

EmitPayload
update:modelValue
[value: string]

Event handler called when the value changes.

Data AttributeValue
[data-orientation]"vertical" | "horizontal"

List

Contains the top level menu items.

PropDefaultType
as
'ul'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

Data AttributeValue
[data-orientation]"vertical" | "horizontal"

Item

A top level menu item, contains a link or trigger with content combination.

PropDefaultType
as
'li'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

value
string

A unique value that associates the item with an active value when the navigation menu is controlled.

This prop is managed automatically when uncontrolled.

Trigger

The button that toggles the content.

PropDefaultType
as
'button'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

disabled
boolean

When true, prevents the user from interacting with item

Data AttributeValue
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Content

Contains the content associated with each trigger.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

disableOutsidePointerEvents
boolean

When true, hover/focus/click interactions will be disabled on elements outside the DismissableLayer. Users will need to click twice on outside elements to interact with them: once to close the DismissableLayer, and again to trigger the element.

forceMount
boolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

EmitPayload
escapeKeyDown
[event: KeyboardEvent]

Event handler called when the escape key is down. Can be prevented.

focusOutside
[event: FocusOutsideEvent]

Event handler called when the focus moves outside of the DismissableLayer. Can be prevented.

interactOutside
[event: PointerDownOutsideEvent | FocusOutsideEvent]

Event handler called when an interaction happens outside the DismissableLayer. Specifically, when a pointerdown event happens outside or focus moves outside of it. Can be prevented.

pointerDownOutside
[event: PointerDownOutsideEvent]

Event handler called when the a pointerdown event happens outside of the DismissableLayer. Can be prevented.

Data AttributeValue
[data-state]"open" | "closed"
[data-motion]"to-start" | "to-end" | "from-start" | "from-end"
[data-orientation]"vertical" | "horizontal"

A navigational link.

PropDefaultType
active
boolean

Used to identify the link as the currently active page.

as
'a'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

EmitPayload
select
[payload: MouseEvent]

Event handler called when the user selects a link (via mouse or keyboard).

Calling event.preventDefault in this handler will prevent the navigation menu from closing when selecting that link.

Data AttributeValue
[data-active]Present when active

Indicator

An optional indicator element that renders below the list, is used to highlight the currently active trigger.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

forceMount
boolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

Data AttributeValue
[data-state]"visible" | "hidden"
[data-orientation]"vertical" | "horizontal"

Viewport

An optional viewport element that is used to render active content outside of the list.

PropDefaultType
as
'div'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

forceMount
boolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

Data AttributeValue
[data-state]"visible" | "hidden"
[data-orientation]"vertical" | "horizontal"
CSS VariableDescription
--radix-navigation-menu-viewport-width
The width of the viewport when visible/hidden, computed from the active content
--radix-navigation-menu-viewport-height
The height of the viewport when visible/hidden, computed from the active content

Examples

Vertical

You can create a vertical menu by using the orientation prop.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuIndicator,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuSub,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot orientation="vertical">
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item one</NavigationMenuTrigger>
        <NavigationMenuContent>Item one content</NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item two</NavigationMenuTrigger>
        <NavigationMenuContent>Item Two content</NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenuRoot>
</template>

Flexible layouts

Use the Viewport part when you need extra control over where Content is rendered. This can be helpful when your design requires an adjusted DOM structure or if you need flexibility to achieve advanced animation. Tab focus will be maintained automatically.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item one</NavigationMenuTrigger>
        <NavigationMenuContent>Item one content</NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item two</NavigationMenuTrigger>
        <NavigationMenuContent>Item two content</NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>

    <!-- NavigationMenuContent will be rendered here when active  -->
    <NavigationMenuViewport />
  </NavigationMenuRoot>
</template>

With indicator

You can use the optional Indicator part to highlight the currently active Trigger, this is useful when you want to provide an animated visual cue such as an arrow or highlight to accompany the Viewport.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item one</NavigationMenuTrigger>
        <NavigationMenuContent>Item one content</NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item two</NavigationMenuTrigger>
        <NavigationMenuContent>Item two content</NavigationMenuContent>
      </NavigationMenuItem>

      <NavigationMenuIndicator class="NavigationMenuIndicator" />
    </NavigationMenuList>

    <NavigationMenuViewport />
  </NavigationMenuRoot>
</template>
css
/* styles.css */
.NavigationMenuIndicator {
  background-color: grey;
}
.NavigationMenuIndicator[data-orientation="horizontal"] {
  height: 3px;
  transition: width, transform, 250ms ease;
}

With submenus

Create a submenu by nesting your NavigationMenu and using the Sub part in place of its Root. Submenus work differently to Root navigation menus and are similar to Tabs in that one item should always be active, so be sure to assign and set a defaultValue.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuSub,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item one</NavigationMenuTrigger>
        <NavigationMenuContent>Item one content</NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item two</NavigationMenuTrigger>
        <NavigationMenuContent>
          <NavigationMenuSub default-value="sub1">
            <NavigationMenuList>
              <NavigationMenuItem value="sub1">
                <NavigationMenuTrigger>Sub item one</NavigationMenuTrigger>
                <NavigationMenuContent> Sub item one content </NavigationMenuContent>
              </NavigationMenuItem>
              <NavigationMenuItem value="sub2">
                <NavigationMenuTrigger>Sub item two</NavigationMenuTrigger>
                <NavigationMenuContent> Sub item two content </NavigationMenuContent>
              </NavigationMenuItem>
            </NavigationMenuList>
          </NavigationMenuSub>
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenuRoot>
</template>

With client side routing

If you need to use the RouterLink component provided by your routing package then we recommend adding asChild="true" to NavigationMenuLink, or setting as="RouterLink". This will ensure accessibility and consistent keyboard control is maintained:

vue
<script setup lang="ts">
import { NavigationMenuItem, NavigationMenuList, NavigationMenuRoot } from 'radix-vue'

// RouterLink should be injected by default if using `vue-router`
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuLink as-child>
          <RouterLink to="/">
            Home
          </RouterLink>
          <NavigationMenuLink />
        </navigationmenulink>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuLink :as="RouterLink" to="/about">
          About
        </NavigationMenuLink>
      </NavigationMenuItem>
    </NavigationMenuList>
  </NavigationMenuRoot>
</template>

Advanced animation

We expose --radix-navigation-menu-viewport-[width|height] and data-motion['from-start'|'to-start'|'from-end'|'to-end'] attributes to allow you to animate Viewport size and Content position based on the enter/exit direction.

Combining these with position: absolute; allows you to create smooth overlapping animation effects when moving between items.

vue
<script setup lang="ts">
import {
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuList,
  NavigationMenuRoot,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from 'radix-vue'
</script>

<template>
  <NavigationMenuRoot>
    <NavigationMenuList>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item one</NavigationMenuTrigger>
        <NavigationMenuContent class="NavigationMenuContent">
          Item one content
        </NavigationMenuContent>
      </NavigationMenuItem>
      <NavigationMenuItem>
        <NavigationMenuTrigger>Item two</NavigationMenuTrigger>
        <NavigationMenuContent class="NavigationMenuContent">
          Item two content
        </NavigationMenuContent>
      </NavigationMenuItem>
    </NavigationMenuList>

    <NavigationMenuViewport class="NavigationMenuViewport" />
  </NavigationMenuRoot>
</template>
css
/* styles.css */
.NavigationMenuContent {
  position: absolute;
  top: 0;
  left: 0;
  animation-duration: 250ms;
  animation-timing-function: ease;
}
.NavigationMenuContent[data-motion="from-start"] {
  animation-name: enterFromLeft;
}
.NavigationMenuContent[data-motion="from-end"] {
  animation-name: enterFromRight;
}
.NavigationMenuContent[data-motion="to-start"] {
  animation-name: exitToLeft;
}
.NavigationMenuContent[data-motion="to-end"] {
  animation-name: exitToRight;
}

.NavigationMenuViewport {
  position: relative;
  width: var(--radix-navigation-menu-viewport-width);
  height: var(--radix-navigation-menu-viewport-height);
  transition: width, height, 250ms ease;
}

@keyframes enterFromRight {
  from {
    opacity: 0;
    transform: translateX(200px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes enterFromLeft {
  from {
    opacity: 0;
    transform: translateX(-200px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes exitToRight {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(200px);
  }
}

@keyframes exitToLeft {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(-200px);
  }
}

Accessibility

Adheres to the navigation role requirements.

Differences to menubar

NavigationMenu should not be confused with menubar, although this primitive shares the name menu in the colloquial sense to refer to a set of navigation links, it does not use the WAI-ARIA menu role. This is because menu and menubars behave like native operating system menus most commonly found in desktop application windows, as such they feature complex functionality like composite focus management and first-character navigation.

These features are often considered unnecessary for website navigation and at worst can confuse users who are familiar with established website patterns.

See the W3C Disclosure Navigation Menu example for more information.

It's important to use NavigationMenuLink for all navigational links within a menu, this not only applies to the main list but also within any content rendered via NavigationMenuContent. This will ensure consistent keyboard interactions and accessibility while also giving access to the active prop for setting aria-current and the active styles. See this example for more information on usage with third party routing components.

Keyboard Interactions

KeyDescription
SpaceEnter
When focus is on NavigationMenuTrigger, opens the content.
Tab
Moves focus to the next focusable element.
ArrowDown
When horizontal and focus is on an open NavigationMenuTrigger, moves focus into NavigationMenuContent.
Moves focus to the next NavigationMenuTrigger or NavigationMenuLink.
ArrowUp
Moves focus to the previous NavigationMenuTrigger or NavigationMenuLink.
ArrowRightArrowLeft
When vertical and focus is on an open NavigationMenuTrigger, moves focus into its NavigationMenuContent.
Moves focus to the next / previous NavigationMenuTrigger or NavigationMenuLink.
HomeEnd
Moves focus to the first/last NavigationMenu.Trigger or NavigationMenu.Link.
Esc
Closes open NavigationMenu.Content and moves focus to its NavigationMenu.Trigger.