Navigation Menu
A collection of links for site navigation.
The Navigation Menu component provides a horizontal navigation bar with dropdown menus. It supports two-column layouts with content sections and navigation items. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/navigation-menu.tsx:
tsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
interface NavigationMenuContextValue {
activeItem: string | null
setActiveItem: (item: string | null) => void
}
const NavigationMenuContext = React.createContext<NavigationMenuContextValue | undefined>(undefined)
const useNavigationMenu = () => {
const context = React.useContext(NavigationMenuContext)
if (!context) {
throw new Error("NavigationMenu components must be used within NavigationMenu")
}
return context
}
interface NavigationMenuProps {
children: React.ReactNode
className?: string
}
const NavigationMenu = ({ children, className }: NavigationMenuProps) => {
const [activeItem, setActiveItem] = React.useState<string | null>(null)
React.useEffect(() => {
if (!activeItem) return
const handleClick = (e: MouseEvent) => {
const target = e.target as Node
const portalContent = document.querySelector('[data-navigation-menu-content]')
if (portalContent && !portalContent.contains(target)) {
const trigger = document.querySelector(`[data-navigation-menu-trigger="${activeItem}"]`)
if (trigger && !trigger.contains(target)) {
setActiveItem(null)
}
}
}
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setActiveItem(null)
}
}
document.addEventListener("mousedown", handleClick, true)
document.addEventListener("keydown", handleEscape)
return () => {
document.removeEventListener("mousedown", handleClick, true)
document.removeEventListener("keydown", handleEscape)
}
}, [activeItem])
return (
<NavigationMenuContext.Provider value={{ activeItem, setActiveItem }}>
<nav className={cn("flex items-center gap-1", className)}>
{children}
</nav>
</NavigationMenuContext.Provider>
)
}
interface NavigationMenuListProps extends React.HTMLAttributes<HTMLUListElement> {}
const NavigationMenuList = React.forwardRef<HTMLUListElement, NavigationMenuListProps>(
({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex items-center gap-1", className)}
{...props}
/>
)
)
NavigationMenuList.displayName = "NavigationMenuList"
interface NavigationMenuItemProps {
value: string
children: React.ReactNode
}
const NavigationMenuItem = ({ value, children }: NavigationMenuItemProps) => {
return <>{children}</>
}
interface NavigationMenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string
}
const NavigationMenuTrigger = React.forwardRef<HTMLButtonElement, NavigationMenuTriggerProps>(
({ className, value, children, ...props }, ref) => {
const { activeItem, setActiveItem } = useNavigationMenu()
const isActive = activeItem === value
const handleClick = () => {
setActiveItem(isActive ? null : value)
}
return (
<button
ref={ref}
data-navigation-menu-trigger={value}
onClick={handleClick}
className={cn(
"px-4 py-2 text-sm font-bold rounded-md border-2 border-transparent transition-colors flex items-center gap-1",
isActive
? "bg-primary text-primary-foreground border-foreground neobrutalism-shadow-sm"
: "hover:bg-muted",
className
)}
{...props}
>
{children}
<span className={cn("transition-transform", isActive && "rotate-180")}>
▼
</span>
</button>
)
}
)
NavigationMenuTrigger.displayName = "NavigationMenuTrigger"
interface NavigationMenuLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
const NavigationMenuLink = React.forwardRef<HTMLAnchorElement, NavigationMenuLinkProps>(
({ className, ...props }, ref) => (
<a
ref={ref}
className={cn(
"px-4 py-2 text-sm font-bold rounded-md border-2 border-transparent transition-colors hover:bg-muted",
className
)}
{...props}
/>
)
)
NavigationMenuLink.displayName = "NavigationMenuLink"
interface NavigationMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string
}
const NavigationMenuContent = React.forwardRef<HTMLDivElement, NavigationMenuContentProps>(
({ className, value, children, ...props }, ref) => {
const { activeItem } = useNavigationMenu()
const contentRef = React.useRef<HTMLDivElement>(null)
const triggerRef = React.useRef<HTMLElement | null>(null)
React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement)
React.useEffect(() => {
if (activeItem === value) {
triggerRef.current = document.querySelector(`[data-navigation-menu-trigger="${value}"]`)
}
}, [activeItem, value])
React.useEffect(() => {
if (activeItem !== value || !contentRef.current || !triggerRef.current) return
const content = contentRef.current
const trigger = triggerRef.current
const triggerRect = trigger.getBoundingClientRect()
const contentRect = content.getBoundingClientRect()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let top = triggerRect.bottom + 8
let left = triggerRect.left
if (left + contentRect.width > viewportWidth) {
left = viewportWidth - contentRect.width - 8
}
if (left < 8) left = 8
if (top + contentRect.height > viewportHeight) {
top = triggerRect.top - contentRect.height - 8
}
if (top < 8) top = 8
content.style.position = "fixed"
content.style.top = `${top}px`
content.style.left = `${left}px`
}, [activeItem, value])
if (activeItem !== value) return null
const content = (
<div
ref={contentRef}
data-navigation-menu-content
className={cn(
"fixed z-50 rounded-md border-2 border-foreground bg-background p-6 neobrutalism-shadow",
className
)}
onMouseDown={(e) => e.stopPropagation()}
{...props}
>
{children}
</div>
)
return ReactDOM.createPortal(content, document.body)
}
)
NavigationMenuContent.displayName = "NavigationMenuContent"
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuContent,
}JavaScript: Copy this code into components/ui/navigation-menu.jsx:
jsx
"use client"
import * as React from "react"
import * as ReactDOM from "react-dom"
import { cn } from "@/lib/utils"
const NavigationMenuContext = React.createContext(undefined)
const useNavigationMenu = () => {
const context = React.useContext(NavigationMenuContext)
if (!context) {
throw new Error("NavigationMenu components must be used within NavigationMenu")
}
return context
}
const NavigationMenu = ({ children, className }) => {
const [activeItem, setActiveItem] = React.useState(null)
React.useEffect(() => {
if (!activeItem) return
const handleClick = (e) => {
const target = e.target
const portalContent = document.querySelector('[data-navigation-menu-content]')
if (portalContent && !portalContent.contains(target)) {
const trigger = document.querySelector(`[data-navigation-menu-trigger="${activeItem}"]`)
if (trigger && !trigger.contains(target)) {
setActiveItem(null)
}
}
}
const handleEscape = (e) => {
if (e.key === "Escape") {
setActiveItem(null)
}
}
document.addEventListener("mousedown", handleClick, true)
document.addEventListener("keydown", handleEscape)
return () => {
document.removeEventListener("mousedown", handleClick, true)
document.removeEventListener("keydown", handleEscape)
}
}, [activeItem])
return (
<NavigationMenuContext.Provider value={{ activeItem, setActiveItem }}>
<nav className={cn("flex items-center gap-1", className)}>
{children}
</nav>
</NavigationMenuContext.Provider>
)
}
const NavigationMenuList = React.forwardRef(
({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex items-center gap-1", className)}
{...props}
/>
)
)
NavigationMenuList.displayName = "NavigationMenuList"
const NavigationMenuItem = ({ value, children }) => {
return <>{children}</>
}
const NavigationMenuTrigger = React.forwardRef(
({ className, value, children, ...props }, ref) => {
const { activeItem, setActiveItem } = useNavigationMenu()
const isActive = activeItem === value
const handleClick = () => {
setActiveItem(isActive ? null : value)
}
return (
<button
ref={ref}
data-navigation-menu-trigger={value}
onClick={handleClick}
className={cn(
"px-4 py-2 text-sm font-bold rounded-md border-2 border-transparent transition-colors flex items-center gap-1",
isActive
? "bg-primary text-primary-foreground border-foreground neobrutalism-shadow-sm"
: "hover:bg-muted",
className
)}
{...props}
>
{children}
<span className={cn("transition-transform", isActive && "rotate-180")}>
▼
</span>
</button>
)
}
)
NavigationMenuTrigger.displayName = "NavigationMenuTrigger"
const NavigationMenuLink = React.forwardRef(
({ className, ...props }, ref) => (
<a
ref={ref}
className={cn(
"px-4 py-2 text-sm font-bold rounded-md border-2 border-transparent transition-colors hover:bg-muted",
className
)}
{...props}
/>
)
)
NavigationMenuLink.displayName = "NavigationMenuLink"
const NavigationMenuContent = React.forwardRef(
({ className, value, children, ...props }, ref) => {
const { activeItem } = useNavigationMenu()
const contentRef = React.useRef(null)
const triggerRef = React.useRef(null)
React.useImperativeHandle(ref, () => contentRef.current)
React.useEffect(() => {
if (activeItem === value) {
triggerRef.current = document.querySelector(`[data-navigation-menu-trigger="${value}"]`)
}
}, [activeItem, value])
React.useEffect(() => {
if (activeItem !== value || !contentRef.current || !triggerRef.current) return
const content = contentRef.current
const trigger = triggerRef.current
const triggerRect = trigger.getBoundingClientRect()
const contentRect = content.getBoundingClientRect()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let top = triggerRect.bottom + 8
let left = triggerRect.left
if (left + contentRect.width > viewportWidth) {
left = viewportWidth - contentRect.width - 8
}
if (left < 8) left = 8
if (top + contentRect.height > viewportHeight) {
top = triggerRect.top - contentRect.height - 8
}
if (top < 8) top = 8
content.style.position = "fixed"
content.style.top = `${top}px`
content.style.left = `${left}px`
}, [activeItem, value])
if (activeItem !== value) return null
const content = (
<div
ref={contentRef}
data-navigation-menu-content
className={cn(
"fixed z-50 rounded-md border-2 border-foreground bg-background p-6 neobrutalism-shadow",
className
)}
onMouseDown={(e) => e.stopPropagation()}
{...props}
>
{children}
</div>
)
return ReactDOM.createPortal(content, document.body)
}
)
NavigationMenuContent.displayName = "NavigationMenuContent"
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuContent,
}Usage
TypeScript:
tsx
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
} from "@/components/ui/navigation-menu"
function MyComponent() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem value="getting-started">
<NavigationMenuTrigger value="getting-started">
Getting started
</NavigationMenuTrigger>
<NavigationMenuContent value="getting-started">
<div className="grid grid-cols-2 gap-6 w-[600px]">
<div>
<h3 className="text-lg font-bold mb-2">Things</h3>
<p className="text-sm text-muted-foreground">
Beautifully designed components built from scratch.
</p>
</div>
<div className="space-y-4">
<div>
<h4 className="font-bold mb-1">Introduction</h4>
<p className="text-sm text-muted-foreground">
Re-usable components built using React and Tailwind CSS.
</p>
</div>
<div>
<h4 className="font-bold mb-1">Installation</h4>
<p className="text-sm text-muted-foreground">
How to install dependencies and structure your app.
</p>
</div>
</div>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.