Skip to content
goodtabs UI
BrowseBrowse components

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

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

PropTypeDefaultDescription
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