Navigation Menu
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.
$ npm add reka-ui
Anatomy
Import all parts and piece them together.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuSub,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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.
Prop | Default | Type |
---|---|---|
as | 'nav' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 | |
disableClickTrigger | false | boolean If |
disableHoverTrigger | false | boolean If |
disablePointerLeaveClose | boolean If | |
modelValue | string The controlled value of the menu item to activate. Can be used as | |
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. |
unmountOnHide | true | boolean When |
Emit | Payload |
---|---|
update:modelValue | [value: string] Event handler called when the value changes. |
Slots (default) | Payload |
---|---|
modelValue | string Current input values |
Data Attribute | Value |
---|---|
[data-orientation] | "vertical" | "horizontal" |
Sub
Signifies a submenu. Use it in place of the root part when nested to create a submenu.
Prop | Default | Type |
---|---|---|
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 | |
orientation | 'horizontal' | 'vertical' | 'horizontal' The orientation of the menu. |
Emit | Payload |
---|---|
update:modelValue | [value: string] Event handler called when the value changes. |
Slots (default) | Payload |
---|---|
modelValue | string Current input values |
Data Attribute | Value |
---|---|
[data-orientation] | "vertical" | "horizontal" |
List
Contains the top level menu items.
Prop | Default | Type |
---|---|---|
as | 'ul' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 Attribute | Value |
---|---|
[data-orientation] | "vertical" | "horizontal" |
Item
A top level menu item, contains a link or trigger with content combination.
Prop | Default | Type |
---|---|---|
as | 'li' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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.
Prop | Default | Type |
---|---|---|
as | 'button' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Content
Contains the content associated with each trigger.
Prop | Default | Type |
---|---|---|
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 | |
forceMount | boolean Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
Emit | Payload |
---|---|
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 |
interactOutside | [event: PointerDownOutsideEvent | FocusOutsideEvent] Event handler called when an interaction happens outside the |
pointerDownOutside | [event: PointerDownOutsideEvent] Event handler called when the a |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-motion] | "to-start" | "to-end" | "from-start" | "from-end" |
[data-orientation] | "vertical" | "horizontal" |
Link
A navigational link.
Prop | Default | Type |
---|---|---|
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 | 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. |
Emit | Payload |
---|---|
select | [payload: CustomEvent<{ originalEvent: Event; }>] Event handler called when the user selects a link (via mouse or keyboard). Calling |
Data Attribute | Value |
---|---|
[data-active] | Present when active |
Indicator
An optional indicator element that renders below the list, is used to highlight the currently active trigger.
Prop | Default | Type |
---|---|---|
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 Attribute | Value |
---|---|
[data-state] | "visible" | "hidden" |
[data-orientation] | "vertical" | "horizontal" |
Viewport
An optional viewport element that is used to render active content outside of the list.
Prop | Default | Type |
---|---|---|
align | 'center' | 'start' | 'center' | 'end' Placement of the viewport for css variables |
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
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 Attribute | Value |
---|---|
[data-state] | "visible" | "hidden" |
[data-orientation] | "vertical" | "horizontal" |
CSS Variable | Description |
---|---|
--reka-navigation-menu-viewport-width | The width of the viewport when visible/hidden, computed from the active content |
--reka-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.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuSub,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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
.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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>
/* 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
.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuSub,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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:
<script setup lang="ts">
import { NavigationMenuItem, NavigationMenuList, NavigationMenuRoot } from 'reka-ui'
// 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 --reka-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.
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'reka-ui'
</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>
/* 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(--reka-navigation-menu-viewport-width);
height: var(--reka-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.
Link usage and aria-current
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
Key | Description |
---|---|
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 .
|