A collection of React components for building accessible, customizable navigation menus with keyboard support and flexible layout options
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Content and viewport components for displaying menu content with animation support and focus management.
Dismissable content panel that displays when a navigation menu item is triggered, with built-in focus management and animation support.
/**
* Content panel for navigation menu items with dismissable layer integration
* @param forceMount - Force mounting for animation control (useful with animation libraries)
*/
const NavigationMenuContent: React.ForwardRefExoticComponent<
NavigationMenuContentProps & React.RefAttributes<HTMLDivElement>
>;
interface NavigationMenuContentProps
extends React.ComponentPropsWithoutRef<"div"> {
forceMount?: true;
}Key Features:
forceMount propUsage Examples:
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink
} from "@radix-ui/react-navigation-menu";
// Basic content usage
function BasicContentExample() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Products</NavigationMenuTrigger>
<NavigationMenuContent className="nav-content">
<div className="content-grid">
<NavigationMenuLink href="/web">Web Development</NavigationMenuLink>
<NavigationMenuLink href="/mobile">Mobile Apps</NavigationMenuLink>
<NavigationMenuLink href="/desktop">Desktop Software</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);
}
// Content with force mount for animations
function AnimatedContentExample() {
return (
<NavigationMenuItem>
<NavigationMenuTrigger>Services</NavigationMenuTrigger>
<NavigationMenuContent
forceMount
className="animated-content"
onPointerEnter={() => console.log("Content entered")}
onPointerLeave={() => console.log("Content left")}
>
<div className="service-list">
<h3>Our Services</h3>
<NavigationMenuLink href="/consulting">Consulting</NavigationMenuLink>
<NavigationMenuLink href="/support">Support</NavigationMenuLink>
<NavigationMenuLink href="/training">Training</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
);
}
// Rich content with complex layout
function RichContentExample() {
return (
<NavigationMenuItem>
<NavigationMenuTrigger>Resources</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="resource-content">
<div className="resource-section">
<h4>Documentation</h4>
<NavigationMenuLink href="/docs/getting-started">Getting Started</NavigationMenuLink>
<NavigationMenuLink href="/docs/api">API Reference</NavigationMenuLink>
<NavigationMenuLink href="/docs/examples">Examples</NavigationMenuLink>
</div>
<div className="resource-section">
<h4>Community</h4>
<NavigationMenuLink href="/forum">Forum</NavigationMenuLink>
<NavigationMenuLink href="/discord">Discord</NavigationMenuLink>
<NavigationMenuLink href="/github">GitHub</NavigationMenuLink>
</div>
<div className="resource-section">
<h4>Latest</h4>
<p>Check out our latest blog post about navigation patterns.</p>
<NavigationMenuLink href="/blog/latest">Read More</NavigationMenuLink>
</div>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
);
}Centralized viewport container that manages and displays menu content with dynamic sizing and positioning.
/**
* Centralized viewport for menu content with dynamic sizing
* Provides a container that automatically sizes to match active content
* @param forceMount - Force mounting for animation control
*/
const NavigationMenuViewport: React.ForwardRefExoticComponent<
NavigationMenuViewportProps & React.RefAttributes<HTMLDivElement>
>;
interface NavigationMenuViewportProps
extends React.ComponentPropsWithoutRef<"div"> {
forceMount?: true;
}Key Features:
Usage Examples:
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuViewport
} from "@radix-ui/react-navigation-menu";
// Basic viewport setup
function ViewportExample() {
return (
<div className="nav-container">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Products</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="products-content">
{/* Content will be rendered in viewport */}
Product information here
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Services</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="services-content">
{/* Different sized content */}
Much larger service information content here
with multiple sections and detailed descriptions
</div>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
{/* Viewport renders all content and manages transitions */}
<NavigationMenuViewport className="nav-viewport" />
</NavigationMenu>
</div>
);
}
// Viewport with custom styling using CSS custom properties
function StyledViewportExample() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Large Content</NavigationMenuTrigger>
<NavigationMenuContent>
<div style={{ width: "400px", height: "300px" }}>
Large content area
</div>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport
className="custom-viewport"
style={{
// CSS custom properties are automatically set:
// --radix-navigation-menu-viewport-width
// --radix-navigation-menu-viewport-height
width: "var(--radix-navigation-menu-viewport-width)",
height: "var(--radix-navigation-menu-viewport-height)",
transition: "width 200ms, height 200ms",
}}
/>
</NavigationMenu>
);
}
// Viewport with animation control
function AnimatedViewportExample() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Animated Content</NavigationMenuTrigger>
<NavigationMenuContent forceMount>
Animated content here
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport
forceMount
onPointerEnter={() => console.log("Viewport entered")}
onPointerLeave={() => console.log("Viewport left")}
/>
</NavigationMenu>
);
}The navigation menu can operate in two content display modes:
Content is rendered directly adjacent to triggers:
<NavigationMenuItem>
<NavigationMenuTrigger>Trigger</NavigationMenuTrigger>
<NavigationMenuContent>
{/* Rendered directly in DOM tree */}
</NavigationMenuContent>
</NavigationMenuItem>Content is rendered centrally in a viewport:
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Trigger</NavigationMenuTrigger>
<NavigationMenuContent>
{/* Rendered via portal into viewport */}
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport />
</NavigationMenu>Benefits of Viewport Mode:
// Pointer events for hover behavior
onPointerEnter?: (event: React.PointerEvent) => void;
onPointerLeave?: (event: React.PointerEvent) => void;
// Focus events for accessibility
onFocusOutside?: (event: Event) => void;
onPointerDownOutside?: (event: Event) => void;
// Keyboard events
onKeyDown?: (event: React.KeyboardEvent) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;// Pointer events
onPointerEnter?: (event: React.PointerEvent) => void;
onPointerLeave?: (event: React.PointerEvent) => void;When using viewport mode, these CSS custom properties are automatically available:
.nav-viewport {
width: var(--radix-navigation-menu-viewport-width);
height: var(--radix-navigation-menu-viewport-height);
transition: width 200ms, height 200ms;
}Content components expose data attributes for styling and animation:
[data-state="open"] { /* Content is visible */ }
[data-state="closed"] { /* Content is hidden */ }
[data-motion="from-start"] { /* Animating in from start */ }
[data-motion="from-end"] { /* Animating in from end */ }
[data-motion="to-start"] { /* Animating out to start */ }
[data-motion="to-end"] { /* Animating out to end */ }
[data-orientation="horizontal"|"vertical"] { /* Content orientation */ }Animation Examples:
.nav-content[data-state="open"] {
animation: slideIn 200ms ease-out;
}
.nav-content[data-state="closed"] {
animation: slideOut 200ms ease-in;
}
.nav-content[data-motion="from-start"] {
animation: slideFromStart 200ms ease-out;
}
.nav-content[data-motion="from-end"] {
animation: slideFromEnd 200ms ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideFromStart {
from { transform: translateX(-20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}[data-state="open"] { /* Viewport contains content */ }
[data-state="closed"] { /* Viewport is empty */ }
[data-orientation="horizontal"|"vertical"] { /* Viewport orientation */ }Viewport Styling:
.nav-viewport[data-state="open"] {
pointer-events: auto;
opacity: 1;
}
.nav-viewport[data-state="closed"] {
pointer-events: none;
opacity: 0;
}
.nav-viewport[data-orientation="horizontal"] {
/* Styles for horizontal viewport */
}
.nav-viewport[data-orientation="vertical"] {
/* Styles for vertical viewport */
}Install with Tessl CLI
npx tessl i tessl/npm-radix-ui--react-navigation-menu