A data-driven header menu with wide panels of grouped, described links, an optional featured card, and an accordion list for small screens.
Open Product to see grouped links and a featured card. Arrow keys move between menus.
import { BarChart3, Blocks, Code2, Layers, Rocket, ShieldCheck, Sparkles, Users, Workflow } from "lucide-react";
import { MegaMenu, type MegaMenuItem } from "@/components/ballmac/mega-menu";
const megaItems: MegaMenuItem[] = [
{
label: "Product",
columns: [
{
title: "Build",
links: [
{ title: "Workspaces", href: "#workspaces", description: "Projects, files and approvals in one place.", icon: <Layers /> },
{ title: "Automations", href: "#automations", description: "Run tasks when things change.", icon: <Workflow />, badge: "New" },
{ title: "API", href: "#api", description: "Build on the same data.", icon: <Code2 /> },
],
},
{
title: "Run",
links: [
{ title: "Analytics", href: "#analytics", description: "Usage and performance at a glance.", icon: <BarChart3 /> },
{ title: "Security", href: "#security", description: "Roles, audit logs and sign-in policies.", icon: <ShieldCheck /> },
{ title: "Integrations", href: "#integrations", description: "Connect the tools you already use.", icon: <Blocks /> },
],
},
],
featured: { title: "What's new", description: "See the latest releases and what they change for your team.", href: "#changelog", media: <Sparkles />, cta: "Read the changelog" },
},
{
label: "Solutions",
columns: [
{
links: [
{ title: "Startups", href: "#startups", description: "Move fast with a small team.", icon: <Rocket /> },
{ title: "Enterprise", href: "#enterprise", description: "Controls for larger organizations.", icon: <Users /> },
],
},
],
},
{ label: "Pricing", href: "#pricing" },
{ label: "Docs", href: "#docs" },
];
export default function MegaMenuDemo() {
return (
<div className="grid min-h-56 w-full max-w-3xl content-start gap-6 rounded-xl border bg-card p-3 shadow-sm">
<div className="flex items-center gap-4 rounded-lg border bg-background px-3 py-1.5">
<span className="text-[15px] font-semibold tracking-tight">Acme</span>
<MegaMenu items={megaItems} label="Main" />
<span className="ms-auto text-xs text-muted-foreground md:hidden">Menu on small screens</span>
</div>
<p className="px-2 text-sm text-muted-foreground">Open Product to see grouped links and a featured card. Arrow keys move between menus.</p>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/mega-menuInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/navigation-menu @ballmac/i18nCopy the source into your project.
components/ballmac/mega-menu.tsx// Ballmac UI: Mega Menu. https://ui.ballmac.com/components/mega-menu "use client"; import * as React from "react"; import { ArrowRight, ChevronDown } from "lucide-react"; import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, } from "@/components/ballmac/navigation-menu"; import { cn } from "@/lib/utils"; import { useMessages } from "@/lib/ballmac/i18n"; type MegaMenuLinkItem = { /** Link title. */ title: string; /** Destination. */ href: string; /** One line under the title. */ description?: string; /** Icon shown in a tile before the title. */ icon?: React.ReactNode; /** Small label after the title, such as "New". */ badge?: string; }; type MegaMenuColumn = { /** Small heading above the links. */ title?: string; links: MegaMenuLinkItem[]; }; type MegaMenuFeatured = { title: string; description: string; href: string; /** Call-to-action text. */ cta?: string; /** Visual area above the text: an icon, illustration or gradient element. */ media?: React.ReactNode; }; type MegaMenuItem = | { label: string; href: string } | { label: string; columns: MegaMenuColumn[]; featured?: MegaMenuFeatured }; type MegaMenuProps = Omit<React.ComponentProps<typeof NavigationMenu>, "children"> & { /** Top-level entries: a plain link (`href`) or a panel (`columns`, optionally `featured`). */ items: MegaMenuItem[]; /** Accessible name of the navigation landmark. */ label?: string; }; function isPanel(item: MegaMenuItem): item is Extract<MegaMenuItem, { columns: MegaMenuColumn[] }> { return "columns" in item; } function LinkCard({ link }: { link: MegaMenuLinkItem }) { return ( <NavigationMenuLink href={link.href} className="group/link flex-row items-start gap-3 rounded-xl p-2.5" > {link.icon && ( <span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-background text-foreground shadow-xs transition-colors group-hover/link:border-ring/40 [&_svg]:size-4"> {link.icon} </span> )} <span className="grid min-w-0 gap-0.5"> <span className="flex items-center gap-2 text-sm font-medium"> {link.title} {link.badge && ( <span className="rounded-full bg-primary/10 px-1.5 py-px text-[10px] font-semibold tracking-wide text-primary uppercase"> {link.badge} </span> )} </span> {link.description && <span className="text-xs leading-snug text-muted-foreground">{link.description}</span>} </span> </NavigationMenuLink> ); } /** * A data-driven mega menu on Navigation Menu: wide panels with grouped, described links and an optional featured card. * Shown from the `md` breakpoint up; pair it with MegaMenuMobileList inside your mobile menu. */ function MegaMenu({ items, label, className, viewportAlign = "center", ...props }: MegaMenuProps) { const msg = useMessages() label ??= msg("mega-menu.label", "Main") return ( <NavigationMenu aria-label={label} viewportAlign={viewportAlign} data-slot="mega-menu" className={cn("hidden md:flex", className)} {...props} > <NavigationMenuList> {items.map((item) => isPanel(item) ? ( <NavigationMenuItem key={item.label}> <NavigationMenuTrigger>{item.label}</NavigationMenuTrigger> <NavigationMenuContent> <div className={cn( "grid gap-2 p-1 md:w-[min(46rem,calc(100vw-3rem))]", item.featured ? "md:grid-cols-[1fr_1fr_14rem]" : item.columns.length > 1 ? "md:grid-cols-2" : "md:w-80", )} > {item.columns.map((column, index) => ( <div key={column.title ?? index} className="grid content-start gap-0.5"> {column.title && ( <p className="px-2.5 pt-1.5 pb-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"> {column.title} </p> )} {column.links.map((link) => ( <LinkCard key={link.href} link={link} /> ))} </div> ))} {item.featured && ( <NavigationMenuLink href={item.featured.href} className="group/featured row-span-1 justify-between gap-3 rounded-xl border bg-gradient-to-br from-primary/10 via-muted/60 to-muted/30 p-4 hover:from-primary/15" > <span className="flex min-h-20 items-center justify-center text-primary [&_svg]:size-10"> {item.featured.media} </span> <span className="grid gap-1"> <span className="text-sm font-semibold">{item.featured.title}</span> <span className="text-xs leading-snug text-muted-foreground">{item.featured.description}</span> <span className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-foreground"> {item.featured.cta ?? msg("mega-menu.learnMore", "Learn more")} <ArrowRight aria-hidden="true" className="size-3 transition-transform group-hover/featured:translate-x-0.5 motion-reduce:transition-none rtl:rotate-180 rtl:group-hover/featured:-translate-x-0.5" /> </span> </span> </NavigationMenuLink> )} </div> </NavigationMenuContent> </NavigationMenuItem> ) : ( <NavigationMenuItem key={item.label}> <NavigationMenuLink href={item.href} topLevel> {item.label} </NavigationMenuLink> </NavigationMenuItem> ), )} </NavigationMenuList> </NavigationMenu> ); } type MegaMenuMobileListProps = React.ComponentProps<"div"> & { items: MegaMenuItem[]; /** Called when a link is chosen, for example to close a sheet. */ onNavigate?: () => void; }; /** The same items as accordions for small screens. Uses native disclosure elements, so it works without JavaScript. */ function MegaMenuMobileList({ items, onNavigate, className, ...props }: MegaMenuMobileListProps) { return ( <div data-slot="mega-menu-mobile" className={cn("grid gap-1", className)} {...props}> {items.map((item) => isPanel(item) ? ( <details key={item.label} className="group/details rounded-lg"> <summary className="flex h-11 cursor-pointer list-none items-center justify-between rounded-lg px-3 text-[15px] font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-details-marker]:hidden"> {item.label} <ChevronDown aria-hidden="true" className="size-4 text-muted-foreground transition-transform group-open/details:rotate-180 motion-reduce:transition-none" /> </summary> <div className="grid gap-0.5 pt-1 pb-2 ps-3"> {item.columns.flatMap((column) => column.links).map((link) => ( <a key={link.href} href={link.href} onClick={onNavigate} className="grid gap-0.5 rounded-lg px-3 py-2 outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50" > <span className="text-sm font-medium">{link.title}</span> {link.description && <span className="text-xs text-muted-foreground">{link.description}</span>} </a> ))} </div> </details> ) : ( <a key={item.label} href={item.href} onClick={onNavigate} className="flex h-11 items-center rounded-lg px-3 text-[15px] font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50" > {item.label} </a> ), )} </div> ); } export { MegaMenu, MegaMenuMobileList, type MegaMenuProps, type MegaMenuItem, type MegaMenuColumn, type MegaMenuLinkItem, type MegaMenuFeatured, type MegaMenuMobileListProps, };Update the import paths to match your project setup.
Usage
import { MegaMenu, MegaMenuMobileList } from "@/components/ballmac/mega-menu"The full example is in the Code tab above.
Examples
Mobile list
import { MegaMenuMobileList, type MegaMenuItem } from "@/components/ballmac/mega-menu";
const items: MegaMenuItem[] = [
{
label: "Product",
columns: [
{
links: [
{ title: "Workspaces", href: "#workspaces", description: "Projects, files and approvals." },
{ title: "Automations", href: "#automations", description: "Run tasks when things change." },
{ title: "Analytics", href: "#analytics", description: "Usage at a glance." },
],
},
],
},
{
label: "Solutions",
columns: [{ links: [{ title: "Startups", href: "#startups" }, { title: "Enterprise", href: "#enterprise" }] }],
},
{ label: "Pricing", href: "#pricing" },
{ label: "Docs", href: "#docs" },
];
export default function MegaMenuStates() {
return (
<div className="w-full max-w-xs rounded-xl border bg-card p-2 shadow-sm">
<MegaMenuMobileList items={items} />
</div>
);
}API reference
<MegaMenu>
| Prop | Type | Default |
|---|---|---|
items*Top-level entries: a plain link (`href`) or a panel (`columns`, optionally `featured`). | MegaMenuItem[] | — |
labelAccessible name of the navigation landmark. | string | — |
<MegaMenuMobileList>
| Prop | Type | Default |
|---|---|---|
items* | MegaMenuItem[] | — |
onNavigateCalled when a link is chosen, for example to close a sheet. | () => void | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space / ArrowDown | Opens the focused panel |
| ArrowLeft / ArrowRight | Moves between top-level items |
| Tab | Enters the panel links |
| Escape | Closes the panel |
Use with AI
Pass items: a plain link ({label, href}) or a panel ({label, columns, featured}). MegaMenuMobileList renders the same data for the mobile menu. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Mega Menu (@ballmac/mega-menu) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Sites with many products or solutions
- Headers that need descriptions beside links
Not for
- A handful of plain links; use navbar
- Application commands; use menubar
Registry JSON: https://ui.ballmac.com/r/mega-menu.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
Pairs well with
Navbar
A responsive site header that gains a border and shadow on scroll, can hide while scrolling down, and turns its links into a sheet menu on small screens.
Navigation Menu
A site navigation bar with flyout panels sharing one animated viewport that resizes to its content, with keyboard and pointer intent handling, built on Radix.
Animated Tabs
Radix Tabs with a pill or underline indicator that slides between triggers on a spring, and panels that fade in. Keeps full keyboard support and ARIA; controlled or uncontrolled.
Back To Top
A floating button that appears after scrolling, draws a progress ring, scrolls smoothly (instantly under reduced motion) and moves focus to the content.