Components
Navigation Menu
Navigation Menu
Animated navigation menu with nested submenus and smooth transitions.
Installation
1
Install the packages
npm i motion clsx tailwind-merge2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
navigation-menu.tsx
"use client";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";
import React, { useState } from "react";
import { FiChevronDown, FiChevronRight } from "react-icons/fi";
interface MenuItem {
label: string;
href?: string;
children?: MenuItem[];
icon?: React.ReactNode;
}
interface NavigationMenuProps {
items?: MenuItem[];
orientation?: "horizontal" | "vertical";
}
const NavigationMenu: React.FC<NavigationMenuProps> = ({
items = [
{
label: "Home",
href: "/",
icon: <span>🏠</span>
},
{
label: "Components",
children: [
{ label: "Buttons", href: "/components/buttons" },
{ label: "Forms", href: "/components/forms" },
{
label: "Layout",
children: [
{ label: "Grid", href: "/components/grid" },
{ label: "Flex", href: "/components/flex" }
]
}
]
},
{
label: "Documentation",
href: "/docs"
}
],
orientation = "horizontal"
}) => {
const [openMenus, setOpenMenus] = useState<Set<string>>(new Set());
const toggleMenu = (label: string) => {
const newOpenMenus = new Set(openMenus);
if (newOpenMenus.has(label)) {
newOpenMenus.delete(label);
} else {
newOpenMenus.add(label);
}
setOpenMenus(newOpenMenus);
};
const renderMenuItem = (item: MenuItem, depth = 0): React.ReactNode => {
const hasChildren = item.children && item.children.length > 0;
const isOpen = openMenus.has(item.label);
return (
<div key={item.label} className="relative">
<motion.div
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer",
"hover:bg-neutral-700 transition-colors",
orientation === "vertical" && "w-full justify-between",
depth > 0 && "ml-4"
)}
onClick={() => hasChildren && toggleMenu(item.label)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.icon && <span className="text-neutral-400">{item.icon}</span>}
<span className="text-neutral-300">{item.label}</span>
{hasChildren && (
<motion.div
animate={{ rotate: isOpen ? 90 : 0 }}
transition={{ duration: 0.2 }}
>
<FiChevronRight className="h-4 w-4 text-neutral-400" />
</motion.div>
)}
</motion.div>
<AnimatePresence>
{hasChildren && isOpen && (
<motion.div
className={cn(
"mt-1",
orientation === "horizontal" && "absolute top-full left-0 z-10",
orientation === "vertical" && "ml-4"
)}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<div className={cn(
"bg-neutral-800 rounded-lg border border-neutral-700 shadow-lg",
orientation === "horizontal" && "min-w-48 p-2",
orientation === "vertical" && "border-l-4 border-l-cyan-500"
)}>
{item.children?.map((child) => renderMenuItem(child, depth + 1))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
return (
<nav className={cn(
"flex gap-2",
orientation === "vertical" && "flex-col"
)}>
{items.map((item) => renderMenuItem(item))}
</nav>
);
};
export default NavigationMenu;"use client";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";
import React, { useState } from "react";
import { FiChevronDown, FiChevronRight } from "react-icons/fi";
interface MenuItem {
label: string;
href?: string;
children?: MenuItem[];
icon?: React.ReactNode;
}
interface NavigationMenuProps {
items?: MenuItem[];
orientation?: "horizontal" | "vertical";
}
const NavigationMenu: React.FC<NavigationMenuProps> = ({
items = [
{
label: "Home",
href: "/",
icon: <span>🏠</span>
},
{
label: "Components",
children: [
{ label: "Buttons", href: "/components/buttons" },
{ label: "Forms", href: "/components/forms" },
{
label: "Layout",
children: [
{ label: "Grid", href: "/components/grid" },
{ label: "Flex", href: "/components/flex" }
]
}
]
},
{
label: "Documentation",
href: "/docs"
}
],
orientation = "horizontal"
}) => {
const [openMenus, setOpenMenus] = useState<Set<string>>(new Set());
const toggleMenu = (label: string) => {
const newOpenMenus = new Set(openMenus);
if (newOpenMenus.has(label)) {
newOpenMenus.delete(label);
} else {
newOpenMenus.add(label);
}
setOpenMenus(newOpenMenus);
};
const renderMenuItem = (item: MenuItem, depth = 0): React.ReactNode => {
const hasChildren = item.children && item.children.length > 0;
const isOpen = openMenus.has(item.label);
return (
<div key={item.label} className="relative">
<motion.div
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer",
"hover:bg-neutral-700 transition-colors",
orientation === "vertical" && "w-full justify-between",
depth > 0 && "ml-4"
)}
onClick={() => hasChildren && toggleMenu(item.label)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.icon && <span className="text-neutral-400">{item.icon}</span>}
<span className="text-neutral-300">{item.label}</span>
{hasChildren && (
<motion.div
animate={{ rotate: isOpen ? 90 : 0 }}
transition={{ duration: 0.2 }}
>
<FiChevronRight className="h-4 w-4 text-neutral-400" />
</motion.div>
)}
</motion.div>
<AnimatePresence>
{hasChildren && isOpen && (
<motion.div
className={cn(
"mt-1",
orientation === "horizontal" && "absolute top-full left-0 z-10",
orientation === "vertical" && "ml-4"
)}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<div className={cn(
"bg-neutral-800 rounded-lg border border-neutral-700 shadow-lg",
orientation === "horizontal" && "min-w-48 p-2",
orientation === "vertical" && "border-l-4 border-l-cyan-500"
)}>
{item.children?.map((child) => renderMenuItem(child, depth + 1))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
return (
<nav className={cn(
"flex gap-2",
orientation === "vertical" && "flex-col"
)}>
{items.map((item) => renderMenuItem(item))}
</nav>
);
};
export default NavigationMenu;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | MenuItem[] | [] | Array of menu items with nested children support. |
| orientation | string | horizontal | Menu orientation (horizontal or vertical). |