A hover-activated nested menu component for multi-level navigation with smooth animations.
Search for a command to run...
A hover-activated nested menu component for multi-level navigation with smooth animations.
Handles complex settings without the clutter. Drill down only when you need to.
A comprehensive settings menu with multiple sections, submenus, and social links - similar to the GAIA app's settings menu.
npx shadcn@latest add https://ui.heygaia.io/r/nested-menu.jsonimport { NestedMenu } from "@/components/ui/nested-menu";
import { Button } from "@/components/ui/button";
import { Settings, ChevronRight } from "lucide-react";
const menuSections = [
{
title: "Actions",
items: [
{
key: "settings",
label: "Settings",
icon: Settings,
onSelect: () => console.log("Settings clicked"),
},
{
key: "download",
label: "Download",
hasSubmenu: true,
submenuItems: [
{ key: "mac", label: "macOS", onSelect: () => {} },
{ key: "windows", label: "Windows", onSelect: () => {} },
],
},
],
},
];
export default function Example() {
return (
<NestedMenu
sections={menuSections}
trigger={<Button>Open Menu</Button>}
arrowIcon={ChevronRight}
/>
);
}For more control, use the useNestedMenu hook:
import { useNestedMenu, NestedMenuTooltip } from "@/components/ui/nested-menu";
function MyComponent() {
const resourcesMenu = useNestedMenu();
return (
<>
<button
onMouseEnter={resourcesMenu.handleMouseEnter}
onMouseLeave={resourcesMenu.handleMouseLeave}
>
Resources
</button>
<NestedMenuTooltip
isOpen={resourcesMenu.isOpen}
onOpenChange={resourcesMenu.setIsOpen}
itemRef={resourcesMenu.itemRef}
menuItems={[
{ key: "docs", label: "Documentation", onSelect: () => {} },
{ key: "blog", label: "Blog", onSelect: () => {} },
]}
onMouseEnter={resourcesMenu.cancelClose}
onMouseLeave={resourcesMenu.handleMouseLeave}
/>
</>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
| sections | NestedMenuSectionProps[] | - | Menu sections (required) |
| trigger | React.ReactNode | - | Trigger element (required) |
| iconClassName | string | "h-4 w-4" | Icon className |
| arrowIcon | React.ComponentType | undefined | Arrow icon for submenu items |
| side | "top" | "right" | "bottom" | "left" | "right" | Menu placement |
| align | "start" | "center" | "end" | "start" | Alignment relative to trigger |
| sideOffset | number | 8 | Offset from trigger |
| className | string | undefined | Container className |
| open | boolean | undefined | Controlled open state |
| onOpenChange | (open: boolean) => void | undefined | Callback when open state changes |
interface NestedMenuItem {
/** Unique key for the item */
key: string;
/** Display label */
label: string;
/** Icon component */
icon?: React.ComponentType<{ className?: string }>;
/** Click handler */
onSelect?: () => void;
/** Whether this item has a submenu */
hasSubmenu?: boolean;
/** Submenu items (if hasSubmenu is true) */
submenuItems?: NestedMenuItem[];
/** Color variant */
variant?: "default" | "danger";
/** Additional className for custom styling */
className?: string;
/** Icon color (CSS color value) */
iconColor?: string;
/** Separator after this item */
separator?: boolean;
}interface NestedMenuSectionProps {
/** Section title */
title?: string;
/** Items in this section */
items: NestedMenuItem[];
/** Show divider after section */
showDivider?: boolean;
}const [open, setOpen] = useState(false);
<NestedMenu
sections={sections}
trigger={<Button>Menu</Button>}
open={open}
onOpenChange={setOpen}
/>const sections = [
{
items: [
{
key: "twitter",
label: "Twitter",
icon: TwitterIcon,
iconColor: "#1da1f2",
onSelect: () => {},
},
{
key: "discord",
label: "Discord",
icon: DiscordIcon,
iconColor: "#5865F2",
onSelect: () => {},
},
],
},
];const sections = [
{
items: [
{
key: "logout",
label: "Sign Out",
icon: LogoutIcon,
variant: "danger",
onSelect: () => {},
},
],
},
];