Mega menu
A top nav with plain links and triggers that open a panel of grouped links.
npx shadcn@latest add https://ui.goodtabs.co/r/mega-menu.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/mega-menu
Northwind
Hover Product, or focus it and press Enter.
A Product trigger with two groups next to Pricing and Docs links. Hover or press Enter to open.
Three columns
Northwind
Three columns at md and up, one under it.
A Solutions panel with three titled groups at md and up.
Links only
Northwind
No panels, just four links with the first marked active.
Usage
import { MegaMenu } from "@/components/ui/mega-menu"
<MegaMenu
items={[
{
label: "Product",
groups: [
{ title: "Platform", links: [{ label: "Issues", href: "/issues", description: "Plan and track work." }] },
{ title: "Workflow", links: [{ label: "API", href: "/docs/api", description: "GraphQL with typed SDKs." }] },
],
},
{ label: "Pricing", href: "/pricing" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ({ label; href; active? } | { label; groups: { title?; links: { label; href; description? }[] }[] })[] | Plain links and panel triggers, in order. Each group is one column. | |
| align | "start" | "center" | "end" | "start" | Where a panel sits under its trigger from md up. |
Source
mega-menu/mega-menu.tsx
import * as React from "react";
import Link from "next/link";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
import { cn } from "@/lib/utils";
export type MegaMenuLink = {
label: string;
href: string;
/** One line under the label inside a panel. */
description?: string;
};
export type MegaMenuGroup = {
/** Mono eyebrow above the column. */
title?: string;
links: MegaMenuLink[];
};
export type MegaMenuItem =
| {
label: string;
href: string;
/** Marks the current page. Sets aria-current and the foreground colour. */
active?: boolean;
}
| {
label: string;
/** Columns in the panel, two or three. */
groups: MegaMenuGroup[];
};
type MegaMenuProps = Omit<React.ComponentProps<typeof NavigationMenu>, "children" | "viewport"> & {
/** Plain links and panel triggers, in order. */
items: MegaMenuItem[];
/** Where a panel sits under its trigger from md up. */
align?: "start" | "center" | "end";
};
const alignClass = {
start: "",
center: "md:left-1/2 md:-translate-x-1/2",
end: "md:right-0 md:left-auto",
};
const columnsClass: Record<number, string> = {
1: "md:w-56 md:grid-cols-1",
2: "md:w-md md:grid-cols-2",
3: "md:w-2xl md:grid-cols-3",
};
/**
* A top nav with plain links and one or two triggers that open a panel of grouped links.
* Panels open on hover, click, Enter or arrow down; Escape closes them.
*/
export function MegaMenu({ items, align = "start", className, ...props }: MegaMenuProps) {
return (
<NavigationMenu viewport={false} data-slot="mega-menu" className={cn("max-w-full", className)} {...props}>
<NavigationMenuList className="gap-1">
{items.map((item) =>
"groups" in item ? (
<NavigationMenuItem key={item.label}>
<NavigationMenuTrigger className="h-8 rounded-sm px-2 font-normal text-muted-foreground hover:text-foreground focus:text-foreground data-popup-open:text-foreground data-open:text-foreground [&_svg]:duration-150">
{item.label}
</NavigationMenuTrigger>
<NavigationMenuContent
className={cn(
"grid gap-x-4 gap-y-1 border p-2 group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-0 group-data-[viewport=false]/navigation-menu:duration-150",
columnsClass[Math.min(item.groups.length, 3)],
alignClass[align],
)}
>
{item.groups.map((group, i) => (
<div key={group.title ?? i} className="flex min-w-0 flex-col gap-px">
{group.title && (
<p className="m-0 px-2 pt-1.5 pb-1 font-mono text-xs tracking-wide text-muted-foreground uppercase">
{group.title}
</p>
)}
{group.links.map((link) => (
<NavigationMenuLink
key={link.href + link.label}
asChild
className="group/mega-menu-link flex flex-col items-start gap-0.5 rounded-md px-2 py-1.5 transition-colors duration-100 ease-out"
>
<Link href={link.href}>
<span className="text-sm font-medium text-foreground">{link.label}</span>
{link.description && (
<span className="line-clamp-1 text-xs text-muted-foreground group-hover/mega-menu-link:text-foreground/70 group-focus/mega-menu-link:text-foreground/70">
{link.description}
</span>
)}
</Link>
</NavigationMenuLink>
))}
</div>
))}
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem key={item.href + item.label}>
<NavigationMenuLink
asChild
className={cn(
"inline-flex h-8 items-center rounded-sm px-2 py-0 text-sm transition-colors duration-100 ease-out hover:text-foreground focus:text-foreground",
item.active ? "font-medium text-foreground" : "text-muted-foreground",
)}
>
<Link href={item.href} aria-current={item.active ? "page" : undefined}>
{item.label}
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
),
)}
</NavigationMenuList>
</NavigationMenu>
);
}
For AI agents
Hand your agent this page, the markdown at /components/mega-menu/prompt.md, or the Copy AI prompt button above.
When to use
- A marketing header where one or two sections have many destinations
- When each destination earns a one-line description
- Next to a logo and actions in a site header from md up
When not to use
- For three to five plain links, use the navbar
- For app navigation with deep levels, use a sidebar or nav tree
- On small screens as the only navigation: panels render in flow there, so pair it with a sheet
Agent notes
- Server component: items are plain data and links use next/link, so pass real hrefs and set active from the pathname
- Panels open on hover and on click, Enter or arrow down; Escape closes. There is no viewport, each panel sits under its own trigger
- Under md the panel renders in flow below the trigger instead of floating, which is fine for a sheet but not for a fixed header
- Give the parent enough room below the header: the panel is absolutely positioned and will be clipped by overflow-hidden