Radix homepage
Components

Navigation Menu

A collection of links for navigating websites.

import React from 'react';
import { styled, keyframes } from '@stitches/react';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { CaretDownIcon } from '@radix-ui/react-icons';
import { violet, mauve, indigo, purple, blackA } from '@radix-ui/colors';
const enterFromRight = keyframes({
from: { transform: 'translateX(200px)', opacity: 0 },
to: { transform: 'translateX(0)', opacity: 1 },
});
const enterFromLeft = keyframes({
from: { transform: 'translateX(-200px)', opacity: 0 },
to: { transform: 'translateX(0)', opacity: 1 },
});
const exitToRight = keyframes({
from: { transform: 'translateX(0)', opacity: 1 },
to: { transform: 'translateX(200px)', opacity: 0 },
});
const exitToLeft = keyframes({
from: { transform: 'translateX(0)', opacity: 1 },
to: { transform: 'translateX(-200px)', opacity: 0 },
});
const scaleIn = keyframes({
from: { transform: 'rotateX(-30deg) scale(0.9)', opacity: 0 },
to: { transform: 'rotateX(0deg) scale(1)', opacity: 1 },
});
const scaleOut = keyframes({
from: { transform: 'rotateX(0deg) scale(1)', opacity: 1 },
to: { transform: 'rotateX(-10deg) scale(0.95)', opacity: 0 },
});
const fadeIn = keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
const fadeOut = keyframes({
from: { opacity: 1 },
to: { opacity: 0 },
});
const StyledMenu = styled(NavigationMenuPrimitive.Root, {
position: 'relative',
display: 'flex',
justifyContent: 'center',
width: '100vw',
zIndex: 1,
});
const StyledList = styled(NavigationMenuPrimitive.List, {
all: 'unset',
display: 'flex',
justifyContent: 'center',
backgroundColor: 'white',
padding: 4,
borderRadius: 6,
listStyle: 'none',
boxShadow: `0 2px 10px ${blackA.blackA7}`,
});
const itemStyles = {
padding: '8px 12px',
outline: 'none',
userSelect: 'none',
fontWeight: 500,
lineHeight: 1,
borderRadius: 4,
fontSize: 15,
color: violet.violet11,
'&:focus': { position: 'relative', boxShadow: `0 0 0 2px ${violet.violet7}` },
'&:hover': { backgroundColor: violet.violet3 },
};
const StyledTrigger = styled(NavigationMenuPrimitive.Trigger, {
all: 'unset',
...itemStyles,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
});
const StyledCaret = styled(CaretDownIcon, {
position: 'relative',
color: violet.violet10,
top: 1,
'[data-state=open] &': { transform: 'rotate(-180deg)' },
'@media (prefers-reduced-motion: no-preference)': {
transition: 'transform 250ms ease',
},
});
const StyledTriggerWithCaret = React.forwardRef(({ children, ...props }, forwardedRef) => (
<StyledTrigger {...props} ref={forwardedRef}>
{children}
<StyledCaret aria-hidden />
</StyledTrigger>
));
const StyledLink = styled(NavigationMenuPrimitive.Link, {
...itemStyles,
display: 'block',
textDecoration: 'none',
fontSize: 15,
lineHeight: 1,
});
const StyledContent = styled(NavigationMenuPrimitive.Content, {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
'@media only screen and (min-width: 600px)': { width: 'auto' },
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '250ms',
animationTimingFunction: 'ease',
'&[data-motion="from-start"]': { animationName: enterFromLeft },
'&[data-motion="from-end"]': { animationName: enterFromRight },
'&[data-motion="to-start"]': { animationName: exitToLeft },
'&[data-motion="to-end"]': { animationName: exitToRight },
},
});
const StyledIndicator = styled(NavigationMenuPrimitive.Indicator, {
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
height: 10,
top: '100%',
overflow: 'hidden',
zIndex: 1,
'@media (prefers-reduced-motion: no-preference)': {
transition: 'width, transform 250ms ease',
'&[data-state="visible"]': { animation: `${fadeIn} 200ms ease` },
'&[data-state="hidden"]': { animation: `${fadeOut} 200ms ease` },
},
});
const StyledArrow = styled('div', {
position: 'relative',
top: '70%',
backgroundColor: 'white',
width: 10,
height: 10,
transform: 'rotate(45deg)',
borderTopLeftRadius: 2,
});
const StyledIndicatorWithArrow = React.forwardRef((props, forwardedRef) => (
<StyledIndicator {...props} ref={forwardedRef}>
<StyledArrow />
</StyledIndicator>
));
const StyledViewport = styled(NavigationMenuPrimitive.Viewport, {
position: 'relative',
transformOrigin: 'top center',
marginTop: 10,
width: '100%',
backgroundColor: 'white',
borderRadius: 6,
overflow: 'hidden',
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
height: 'var(--radix-navigation-menu-viewport-height)',
'@media only screen and (min-width: 600px)': {
width: 'var(--radix-navigation-menu-viewport-width)',
},
'@media (prefers-reduced-motion: no-preference)': {
transition: 'width, height, 300ms ease',
'&[data-state="open"]': { animation: `${scaleIn} 200ms ease` },
'&[data-state="closed"]': { animation: `${scaleOut} 200ms ease` },
},
});
// Exports
const NavigationMenu = StyledMenu;
const NavigationMenuList = StyledList;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const NavigationMenuTrigger = StyledTriggerWithCaret;
const NavigationMenuLink = StyledLink;
const NavigationMenuContent = StyledContent;
const NavigationMenuViewport = StyledViewport;
const NavigationMenuIndicator = StyledIndicatorWithArrow;
// Your app...
const ContentList = styled('ul', {
display: 'grid',
padding: 22,
margin: 0,
columnGap: 10,
listStyle: 'none',
variants: {
layout: {
one: {
'@media only screen and (min-width: 600px)': {
width: 500,
gridTemplateColumns: '.75fr 1fr',
},
},
two: {
'@media only screen and (min-width: 600px)': {
width: 600,
gridAutoFlow: 'column',
gridTemplateRows: 'repeat(3, 1fr)',
},
},
},
},
});
const ListItem = styled('li', {});
const LinkTitle = styled('div', {
fontWeight: 500,
lineHeight: 1.2,
marginBottom: 5,
color: violet.violet12,
});
const LinkText = styled('p', {
all: 'unset',
color: mauve.mauve11,
lineHeight: 1.4,
fontWeight: 'initial',
});
const ContentListItem = React.forwardRef(({ children, title, ...props }, forwardedRef) => (
<ListItem>
<NavigationMenuLink {...props} ref={forwardedRef} css={{ padding: 12, borderRadius: 6, '&:hover': { backgroundColor: mauve.mauve3 }, }} >
<LinkTitle>{title}</LinkTitle>
<LinkText>{children}</LinkText>
</NavigationMenuLink>
</ListItem>
));
const ContentListItemCallout = React.forwardRef(({ children, ...props }, forwardedRef) => (
<ListItem css={{ gridRow: 'span 3' }}>
<NavigationMenuLink {...props} href="/" ref={forwardedRef} css={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column', width: '100%', height: '100%', background: `linear-gradient(135deg, ${purple.purple9} 0%, ${indigo.indigo9} 100%);`, borderRadius: 6, padding: 25, }} >
<svg aria-hidden width="38" height="38" viewBox="0 0 25 25" fill="white">
<path d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"></path>
<path d="M12 0H4V8H12V0Z"></path>
<path d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"></path>
</svg>
<LinkTitle css={{ fontSize: 18, color: 'white', marginTop: 16, marginBottom: 7, }} >
Radix Primitives
</LinkTitle>
<LinkText css={{ fontSize: 14, color: mauve.mauve4, lineHeight: 1.3, }} >
Unstyled, accessible components for React.
</LinkText>
</NavigationMenuLink>
</ListItem>
));
const ViewportPosition = styled('div', {
position: 'absolute',
display: 'flex',
justifyContent: 'center',
width: '100%',
top: '100%',
left: 0,
perspective: '2000px',
});
export const NavigationMenuDemo = () => {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Learn</NavigationMenuTrigger>
<NavigationMenuContent>
<ContentList layout="one">
<ContentListItemCallout />
<ContentListItem href="https://stitches.dev/" title="Stitches">
CSS-in-JS with best-in-class developer experience.
</ContentListItem>
<ContentListItem href="/colors" title="Colors">
Beautiful, thought-out palettes with auto dark mode.
</ContentListItem>
<ContentListItem href="https://icons.radix-ui.com/" title="Icons">
A crisp set of 15x15 icons, balanced and consistent.
</ContentListItem>
</ContentList>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Overview</NavigationMenuTrigger>
<NavigationMenuContent>
<ContentList layout="two">
<ContentListItem title="Introduction" href="/docs/primitives/overview/introduction">
Build high-quality, accessible design systems and web apps.
</ContentListItem>
<ContentListItem title="Getting started" href="/docs/primitives/overview/getting-started" >
A quick tutorial to get you up and running with Radix Primitives.
</ContentListItem>
<ContentListItem title="Styling" href="/docs/primitives/overview/styling">
Unstyled and compatible with any styling solution.
</ContentListItem>
<ContentListItem title="Animation" href="/docs/primitives/overview/animation">
Use CSS keyframes or any animation library of your choice.
</ContentListItem>
<ContentListItem title="Accessibility" href="/docs/primitives/overview/accessibility">
Tested in a range of browsers and assistive technologies.
</ContentListItem>
<ContentListItem title="Releases" href="/docs/primitives/overview/releases">
Radix Primitives releases and their changelogs.
</ContentListItem>
</ContentList>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="https://github.com/radix-ui">Github</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuIndicator />
</NavigationMenuList>
<ViewportPosition>
<NavigationMenuViewport />
</ViewportPosition>
</NavigationMenu>
);
};
export default NavigationMenuDemo;

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.

Install the component from your command line.

npm install @radix-ui/react-navigation-menu

Import all parts and piece them together.

import * as NavigationMenu from '@radix-ui/react-navigation-menu';
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger />
<NavigationMenu.Content>
<NavigationMenu.Link />
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link />
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger />
<NavigationMenu.Content>
<NavigationMenu.Sub>
<NavigationMenu.List />
<NavigationMenu.Viewport />
</NavigationMenu.Sub>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Indicator />
</NavigationMenu.List>
<NavigationMenu.Viewport />
</NavigationMenu.Root>
);

Contains all the parts of a navigation menu.

PropTypeDefault
defaultValuestringNo default value
valuestringNo default value
onValueChangefunctionNo default value
direnumNo default value
orientationenum"horizontal"
Data AttributeValues
[data-orientation]"vertical" | "horizontal"

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

PropTypeDefault
defaultValuestringNo default value
valuestringNo default value
onValueChangefunctionNo default value
orientationenum"horizontal"
Data AttributeValues
[data-orientation]"vertical" | "horizontal"

Contains the top level menu items.

PropTypeDefault
asChildbooleanfalse
Data AttributeValues
[data-orientation]"vertical" | "horizontal"

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

PropTypeDefault
asChildbooleanfalse
valuestringNo default value

The button that toggles the content.

PropTypeDefault
asChildbooleanfalse
Data AttributeValues
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Contains the content associated with each trigger.

PropTypeDefault
asChildbooleanfalse
disableOutsidePointerEventsbooleanfalse
onEscapeKeyDownfunctionNo default value
onPointerDownOutsidefunctionNo default value
onFocusOutsidefunctionNo default value
onInteractOutsidefunctionNo default value
forceMountbooleanNo default value
Data AttributeValues
[data-state]"open" | "closed"
[data-motion]"to-start" | "to-end" | "from-start" | "from-end"
[data-orientation]"vertical" | "horizontal"

A navigational link.

PropTypeDefault
asChildbooleanfalse
activebooleanfalse
onSelectfunctionNo default value
Data AttributeValues
[data-active]Present when active

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

PropTypeDefault
asChildbooleanfalse
forceMountbooleanNo default value
Data AttributeValues
[data-state]"visible" | "hidden"
[data-orientation]"vertical" | "horizontal"

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

PropTypeDefault
asChildbooleanfalse
forceMountbooleanNo default value
Data AttributeValues
[data-state]"visible" | "hidden"
[data-orientation]"vertical" | "horizontal"
CSS VariableDescription
--radix-navigation-menu-viewport-widthThe width of the viewport when visible/hidden, computed from the active content
--radix-navigation-menu-viewport-heightThe height of the viewport when visible/hidden, computed from the active content

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

<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item Two content</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>

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.

<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item two content</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
{/* NavigationMenu.Content will be rendered here when active */}
<NavigationMenu.Viewport />
</NavigationMenu.Root>

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.

import { styled } from '@stitches/react';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
const StyledIndicator = styled(NavigationMenu.Link, {
backgroundColor: 'grey',
'&[data-orientation="horizontal"]': { height: 3 },
'@media (prefers-reduced-motion: no-preference)': {
transition: 'width, transform, 250ms ease',
},
});
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item two content</NavigationMenu.Content>
</NavigationMenu.Item>
<StyledIndicator />
</NavigationMenu.List>
<NavigationMenu.Viewport />
</NavigationMenu.Root>
);

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.

<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Sub defaultValue="sub1">
<NavigationMenu.List>
<NavigationMenu.Item value="sub1">
<NavigationMenu.Trigger>Sub item one</NavigationMenu.Trigger>
<NavigationMenu.Content>
Sub item one content
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item value="sub2">
<NavigationMenu.Trigger>Sub item two</NavigationMenu.Trigger>
<NavigationMenu.Content>
Sub item two content
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Sub>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>

If you need to use the Link component provided by your routing package then we recommend composing with NavigationMenu.Link via a custom component. This will ensure accessibility and consistent keyboard control is maintained. Here's an example using Stitches and Next.js:

import { styled } from '@stitches/react';
import { useRouter } from 'next/router'
import Link as NextLink from 'next/link';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
const StyledNavigationMenuLink = styled(NavigationMenu.Link, {
textDecoration: 'none',
'&[data-active]': { textDecoration: 'underline' },
});
const Link = ({ href, ...props }) => {
const router = useRouter();
const isActive = router.asPath === href;
return (
<NextLink href={href} passHref>
<StyledNavigationMenuLink active={isActive} {...props} />
</NextLink>
);
};
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<Link href="/">Home</Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link href="/about">About</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
);

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. Here's an example using Stitches:

import { styled, keyframes } from '@stitches/react';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
const enterFromLeft = css.keyframes({
from: { transform: 'translateX(-100%)', opacity: 0 },
to: { transform: 'translateX(0)', opacity: 1 },
});
const enterFromRight = css.keyframes({
from: { transform: 'translateX(100%)', opacity: 0 },
to: { transform: 'translateX(0)', opacity: 1 },
});
const exitToLeft = css.keyframes({
from: { transform: 'translateX(0)', opacity: 1 },
to: { transform: 'translateX(-100%)', opacity: 0 },
});
const exitToRight = css.keyframes({
from: { transform: 'translateX(0)', opacity: 1 },
to: { transform: 'translateX(100%)', opacity: 0 },
});
const StyledContent = styled(NavigationMenu.Root, {
position: 'absolute',
top: 0,
left: 0,
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '250ms',
animationTimingFunction: 'ease',
'&[data-motion="from-start"]': { animationName: enterFromLeft },
'&[data-motion="from-end"]': { animationName: enterFromRight },
'&[data-motion="to-start"]': { animationName: exitToLeft },
'&[data-motion="to-end"]': { animationName: exitToRight },
},
});
const StyledViewport = styled(NavigationMenu.Viewport, {
position: 'relative',
width: 'var(--radix-hover-menu-viewport-width)',
height: 'var(--radix-hover-menu-viewport-height)',
'@media (prefers-reduced-motion: no-preference)': {
transition: 'width, height, 250ms ease',
},
});
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<StyledContent>Item one content</StyledContent>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<StyledContent>Item two content</StyledContent>
</NavigationMenu.Item>
</NavigationMenu.List>
<StyledViewport />
</NavigationMenu.Root>
);

Adheres to the navigation role requirements.

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 NavigationMenu.Link for all navigational links within a menu, this not only applies to the main list but also within any content rendered via NavigationMenu.Content. 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.

KeyDescription
SpaceEnterWhen focus is on NavigationMenu.Trigger, opens the content.
TabMoves focus to the next focusable element.
ArrowDownWhen horizontal and focus is on an open NavigationMenu.Trigger, moves focus into NavigationMenu.Content.
Moves focus to the next NavigationMenu.Trigger or NavigationMenu.Link.
ArrowUpMoves focus to the previous NavigationMenu.Trigger or NavigationMenu.Link.
ArrowRightArrowLeftWhen vertical and focus is on an open NavigationMenu.Trigger, moves focus into its NavigationMenu.Content.
Moves focus to the next / previous NavigationMenu.Trigger or NavigationMenu.Link.
HomeEndMoves focus to the first/last NavigationMenu.Trigger or NavigationMenu.Link.
EscCloses open NavigationMenu.Content and moves focus to its NavigationMenu.Trigger.