Mega Menu
Original · freemega menu with grouped links
byWeb InnoventixReact + Tailwind
menumegamenus
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/menu-mega.jsonmenu-mega.tsx
"use client";
import {
useEffect,
useId,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type IconName =
| "flow"
| "pipe"
| "bolt"
| "lock"
| "gauge"
| "route"
| "list"
| "bell"
| "code"
| "chart"
| "gear"
| "rocket"
| "building"
| "package"
| "terminal"
| "book"
| "play"
| "users"
| "briefcase"
| "mail";
type MenuLink = {
label: string;
description: string;
href: string;
icon: IconName;
};
type MenuGroup = {
heading: string;
links: MenuLink[];
};
type FeaturedTone = "indigo" | "violet" | "emerald" | "sky";
type Featured = {
eyebrow: string;
title: string;
body: string;
cta: string;
href: string;
tone: FeaturedTone;
};
type MenuPanel = {
groups: MenuGroup[];
featured: Featured;
};
type MenuItem = {
id: string;
label: string;
href?: string;
panel?: MenuPanel;
};
const svgProps = {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.6,
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": true,
} as const;
const icons: Record<IconName, ReactNode> = {
flow: (
<svg {...svgProps}>
<circle cx="5" cy="6" r="2" />
<circle cx="5" cy="18" r="2" />
<circle cx="19" cy="12" r="2" />
<path d="M7 6h4a3 3 0 0 1 3 3v0M7 18h4a3 3 0 0 0 3-3v0M14 12h3" />
</svg>
),
pipe: (
<svg {...svgProps}>
<path d="M4 8h9a3 3 0 0 1 0 6H6a3 3 0 0 0 0 6h13" />
<path d="M4 8V5m0 3h3m9 12h4l-2-2m2 2-2 2" />
</svg>
),
bolt: (
<svg {...svgProps}>
<path d="M13 2 4 14h6l-1 8 9-12h-6z" />
</svg>
),
lock: (
<svg {...svgProps}>
<rect x="4" y="10" width="16" height="11" rx="2" />
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 15v2" />
</svg>
),
gauge: (
<svg {...svgProps}>
<path d="M4 18a8 8 0 1 1 16 0" />
<path d="M12 18l4-5" />
<circle cx="12" cy="18" r="1.2" />
</svg>
),
route: (
<svg {...svgProps}>
<circle cx="6" cy="18" r="2" />
<circle cx="18" cy="6" r="2" />
<path d="M8 18h5a3 3 0 0 0 3-3V8M12 6H8a2 2 0 0 0 0 4h1" />
</svg>
),
list: (
<svg {...svgProps}>
<path d="M9 6h11M9 12h11M9 18h11" />
<path d="M4 6h.01M4 12h.01M4 18h.01" />
</svg>
),
bell: (
<svg {...svgProps}>
<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z" />
<path d="M10 20a2 2 0 0 0 4 0" />
</svg>
),
code: (
<svg {...svgProps}>
<path d="m8 8-4 4 4 4M16 8l4 4-4 4M13 5l-2 14" />
</svg>
),
chart: (
<svg {...svgProps}>
<path d="M4 4v16h16" />
<path d="M8 15v-3M12 15V8M16 15v-6" />
</svg>
),
gear: (
<svg {...svgProps}>
<circle cx="12" cy="12" r="3" />
<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" />
</svg>
),
rocket: (
<svg {...svgProps}>
<path d="M12 3c3 1 5 4 5 8 0 2-.6 3.6-1.5 5H8.5C7.6 14.6 7 13 7 11c0-4 2-7 5-8Z" />
<circle cx="12" cy="10" r="1.5" />
<path d="M8.5 16 6 19M15.5 16 18 19M12 16v4" />
</svg>
),
building: (
<svg {...svgProps}>
<rect x="5" y="3" width="14" height="18" rx="1.5" />
<path d="M9 7h2M13 7h2M9 11h2M13 11h2M9 15h2M13 15h2M10 21v-3h4v3" />
</svg>
),
package: (
<svg {...svgProps}>
<path d="M12 3 4 7v10l8 4 8-4V7z" />
<path d="M4 7l8 4 8-4M12 11v10" />
</svg>
),
terminal: (
<svg {...svgProps}>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="m7 10 3 2-3 2M13 14h4" />
</svg>
),
book: (
<svg {...svgProps}>
<path d="M5 4h9a3 3 0 0 1 3 3v13a2 2 0 0 0-2-2H5z" />
<path d="M17 7v13M5 4v14" />
</svg>
),
play: (
<svg {...svgProps}>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="m10 9 5 3-5 3z" />
</svg>
),
users: (
<svg {...svgProps}>
<circle cx="9" cy="8" r="3" />
<path d="M3 20a6 6 0 0 1 12 0M16 5.5a3 3 0 0 1 0 5M21 20a6 6 0 0 0-4-5.6" />
</svg>
),
briefcase: (
<svg {...svgProps}>
<rect x="3" y="7" width="18" height="13" rx="2" />
<path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2M3 12h18" />
</svg>
),
mail: (
<svg {...svgProps}>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="m4 7 8 6 8-6" />
</svg>
),
};
const menus: MenuItem[] = [
{
id: "products",
label: "Products",
panel: {
groups: [
{
heading: "Platform",
links: [
{
label: "Workflow Engine",
description: "Orchestrate multi-step jobs with retries and backfills.",
href: "#workflow-engine",
icon: "flow",
},
{
label: "Data Pipelines",
description: "Move and reshape data across 200+ connectors.",
href: "#data-pipelines",
icon: "pipe",
},
{
label: "Event Router",
description: "Fan out events to any downstream in real time.",
href: "#event-router",
icon: "bolt",
},
{
label: "Secrets Vault",
description: "Store credentials with per-environment scoping.",
href: "#secrets-vault",
icon: "lock",
},
],
},
{
heading: "Observability",
links: [
{
label: "Metrics",
description: "Track latency and throughput on custom dashboards.",
href: "#metrics",
icon: "gauge",
},
{
label: "Distributed Tracing",
description: "Follow one request across every service hop.",
href: "#tracing",
icon: "route",
},
{
label: "Log Streams",
description: "Search structured logs with sub-second queries.",
href: "#log-streams",
icon: "list",
},
{
label: "Alerting",
description: "Route incidents to on-call before users notice.",
href: "#alerting",
icon: "bell",
},
],
},
],
featured: {
eyebrow: "New",
title: "Cascade Copilot",
body: "Describe a pipeline in plain English and ship it in minutes.",
cta: "Join the beta",
href: "#copilot",
tone: "indigo",
},
},
},
{
id: "solutions",
label: "Solutions",
panel: {
groups: [
{
heading: "By team",
links: [
{
label: "Engineering",
description: "Ship reliable backends without the glue code.",
href: "#engineering",
icon: "code",
},
{
label: "Data & Analytics",
description: "Give analysts self-serve, governed data.",
href: "#analytics",
icon: "chart",
},
{
label: "Platform Ops",
description: "Standardize infrastructure across every squad.",
href: "#platform-ops",
icon: "gear",
},
{
label: "Security",
description: "Enforce policy from first commit to production.",
href: "#security",
icon: "lock",
},
],
},
{
heading: "By stage",
links: [
{
label: "Startups",
description: "A free tier that scales the day you do.",
href: "#startups",
icon: "rocket",
},
{
label: "Scale-ups",
description: "Add SSO, audit logs, and granular roles.",
href: "#scale-ups",
icon: "chart",
},
{
label: "Enterprise",
description: "Dedicated tenancy with a 99.99% uptime SLA.",
href: "#enterprise",
icon: "building",
},
{
label: "Migrations",
description: "Move off legacy schedulers in a single weekend.",
href: "#migrations",
icon: "pipe",
},
],
},
],
featured: {
eyebrow: "Customer story",
title: "Meridian cut batch time 74%",
body: "The data team replaced cron sprawl with one Cascade project.",
cta: "Read the case study",
href: "#meridian",
tone: "emerald",
},
},
},
{
id: "developers",
label: "Developers",
panel: {
groups: [
{
heading: "Build",
links: [
{
label: "API Reference",
description: "Every endpoint with copy-paste examples.",
href: "#api",
icon: "code",
},
{
label: "SDKs & Libraries",
description: "Typed clients for TypeScript, Go, and Python.",
href: "#sdks",
icon: "package",
},
{
label: "Command Line",
description: "Manage projects and deploys from your terminal.",
href: "#cli",
icon: "terminal",
},
{
label: "Webhooks",
description: "Subscribe to platform events over HTTPS.",
href: "#webhooks",
icon: "bolt",
},
],
},
{
heading: "Learn",
links: [
{
label: "Quickstart",
description: "Deploy your first workflow in under ten minutes.",
href: "#quickstart",
icon: "rocket",
},
{
label: "Tutorials",
description: "Guided builds for common integration patterns.",
href: "#tutorials",
icon: "book",
},
{
label: "Sample Apps",
description: "Clone a working project and make it yours.",
href: "#samples",
icon: "package",
},
{
label: "Changelog",
description: "See exactly what shipped this week.",
href: "#changelog",
icon: "list",
},
],
},
],
featured: {
eyebrow: "Open source",
title: "Cascade CLI on GitHub",
body: "Star the repo and follow the roadmap in the open.",
cta: "View the repository",
href: "#github",
tone: "sky",
},
},
},
{
id: "resources",
label: "Resources",
panel: {
groups: [
{
heading: "Learn",
links: [
{
label: "Blog",
description: "Engineering deep-dives and product news.",
href: "#blog",
icon: "book",
},
{
label: "Guides",
description: "Long-form playbooks for data teams.",
href: "#guides",
icon: "book",
},
{
label: "Webinars",
description: "Live sessions with the people who build Cascade.",
href: "#webinars",
icon: "play",
},
{
label: "Community",
description: "Ask questions in the Cascade forum.",
href: "#community",
icon: "users",
},
],
},
{
heading: "Company",
links: [
{
label: "About",
description: "Why we're building the workflow layer.",
href: "#about",
icon: "building",
},
{
label: "Careers",
description: "Remote-first roles across every team.",
href: "#careers",
icon: "briefcase",
},
{
label: "Customers",
description: "Teams shipping on Cascade today.",
href: "#customers",
icon: "users",
},
{
label: "Contact",
description: "Talk to sales or reach support fast.",
href: "#contact",
icon: "mail",
},
],
},
],
featured: {
eyebrow: "Newsletter",
title: "The Dispatch",
body: "A short monthly note on reliable data infrastructure.",
cta: "Subscribe free",
href: "#dispatch",
tone: "violet",
},
},
},
{
id: "pricing",
label: "Pricing",
href: "#pricing",
},
];
const toneGradient: Record<FeaturedTone, string> = {
indigo: "from-indigo-500 to-violet-600",
violet: "from-violet-500 to-indigo-600",
emerald: "from-emerald-500 to-sky-600",
sky: "from-sky-500 to-indigo-600",
};
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={`ml-1 h-3.5 w-3.5 transition-transform duration-200 ${
open ? "rotate-180" : ""
}`}
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function ArrowIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4 transition-transform duration-200 group-hover/feat:translate-x-0.5"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
}
export default function MenuMega() {
const reduced = useReducedMotion();
const baseId = useId();
const panelId = `${baseId}-panel`;
const [openId, setOpenId] = useState<string | null>(null);
const [mobileOpen, setMobileOpen] = useState(false);
const [mobileSection, setMobileSection] = useState<string | null>(null);
const mobilePanelId = `${baseId}-mobile`;
const containerRef = useRef<HTMLElement | null>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
const itemRefs = useRef<(HTMLElement | null)[]>([]);
const openRef = useRef<string | null>(null);
const focusPanelRef = useRef(false);
const closeTimer = useRef<number | null>(null);
useEffect(() => {
openRef.current = openId;
}, [openId]);
useEffect(() => {
if (openId && focusPanelRef.current) {
focusPanelRef.current = false;
const first = panelRef.current?.querySelector<HTMLElement>(
'a[href], button:not([disabled])',
);
first?.focus();
}
}, [openId]);
useEffect(() => {
const onPointerDown = (event: PointerEvent) => {
if (!openRef.current) return;
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setOpenId(null);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && openRef.current) {
setOpenId(null);
}
};
document.addEventListener("pointerdown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("pointerdown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, []);
const clearCloseTimer = () => {
if (closeTimer.current !== null) {
window.clearTimeout(closeTimer.current);
closeTimer.current = null;
}
};
const focusItem = (index: number) => {
const count = menus.length;
const next = ((index % count) + count) % count;
itemRefs.current[next]?.focus();
};
const openWithFocus = (id: string) => {
focusPanelRef.current = true;
setOpenId(id);
};
const closeAndReturn = (index: number) => {
setOpenId(null);
itemRefs.current[index]?.focus();
};
const handleTopKeyDown = (
event: ReactKeyboardEvent<HTMLElement>,
index: number,
item: MenuItem,
) => {
switch (event.key) {
case "ArrowRight":
event.preventDefault();
focusItem(index + 1);
break;
case "ArrowLeft":
event.preventDefault();
focusItem(index - 1);
break;
case "Home":
event.preventDefault();
focusItem(0);
break;
case "End":
event.preventDefault();
focusItem(menus.length - 1);
break;
case "Escape":
if (openId === item.id) {
event.preventDefault();
setOpenId(null);
}
break;
case "ArrowDown":
if (item.panel) {
event.preventDefault();
openWithFocus(item.id);
}
break;
case "Enter":
case " ":
if (item.panel) {
event.preventDefault();
if (openId === item.id) {
setOpenId(null);
} else {
openWithFocus(item.id);
}
}
break;
default:
break;
}
};
const handlePanelKeyDown = (
event: ReactKeyboardEvent<HTMLDivElement>,
index: number,
) => {
if (event.key === "Escape") {
event.preventDefault();
closeAndReturn(index);
}
};
const handleItemPointerEnter = (
event: ReactPointerEvent<HTMLElement>,
item: MenuItem,
) => {
if (event.pointerType !== "mouse") return;
clearCloseTimer();
focusPanelRef.current = false;
if (item.panel) {
setOpenId(item.id);
} else {
setOpenId(null);
}
};
const handleNavPointerLeave = (event: ReactPointerEvent<HTMLElement>) => {
if (event.pointerType !== "mouse") return;
clearCloseTimer();
closeTimer.current = window.setTimeout(() => setOpenId(null), 140);
};
const activeMenu = menus.find((menu) => menu.id === openId && menu.panel);
const activeIndex = menus.findIndex((menu) => menu.id === openId);
const panelVariants = reduced
? {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
}
: {
initial: { opacity: 0, y: 10, scale: 0.985 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: 6, scale: 0.99 },
};
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes cscdGlow {
0%, 100% { transform: translate3d(-9%, 0, 0) scale(1); opacity: .5; }
50% { transform: translate3d(9%, 0, 0) scale(1.08); opacity: .85; }
}
@keyframes cscdSheen {
0% { transform: translateX(-130%) skewX(-16deg); }
60%, 100% { transform: translateX(160%) skewX(-16deg); }
}
.cscd-glow { animation: cscdGlow 8s ease-in-out infinite; }
.cscd-sheen { animation: cscdSheen 4.5s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.cscd-glow, .cscd-sheen { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(60%_60%_at_50%_0%,rgba(99,102,241,0.12),transparent_70%)] dark:bg-[radial-gradient(60%_60%_at_50%_0%,rgba(99,102,241,0.16),transparent_70%)]"
/>
<div className="mx-auto max-w-6xl">
<nav
ref={containerRef}
aria-label="Primary"
onPointerLeave={handleNavPointerLeave}
onPointerEnter={clearCloseTimer}
className="relative"
>
<div className="flex items-center gap-2 rounded-2xl border border-slate-200/80 bg-white/85 px-3 py-2.5 shadow-lg shadow-slate-900/5 backdrop-blur-md sm:gap-3 sm:px-4 dark:border-white/10 dark:bg-slate-900/70 dark:shadow-black/30">
<a
href="#home"
onClick={(event) => {
event.preventDefault();
setOpenId(null);
}}
className="group flex shrink-0 items-center gap-2 rounded-lg px-1.5 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<span className="grid h-8 w-8 place-items-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-sm">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-5 w-5"
>
<path d="M4 8c4 0 4-4 8-4M4 14c4 0 4-4 8-4M4 20c4 0 4-4 8-4M12 4c4 0 4 4 8 4M12 10c4 0 4 4 8 4" />
</svg>
</span>
<span className="text-[15px] font-semibold tracking-tight">
Cascade
</span>
</a>
<ul className="mx-1 hidden items-center gap-0.5 md:flex">
{menus.map((item, index) => {
const isOpen = openId === item.id;
if (!item.panel) {
return (
<li key={item.id}>
<a
ref={(el) => {
itemRefs.current[index] = el;
}}
href={item.href}
onClick={(event) => {
event.preventDefault();
setOpenId(null);
}}
onKeyDown={(event) =>
handleTopKeyDown(event, index, item)
}
onPointerEnter={(event) =>
handleItemPointerEnter(event, item)
}
className="inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus-visible:ring-offset-slate-900"
>
{item.label}
</a>
</li>
);
}
return (
<li key={item.id}>
<button
type="button"
ref={(el) => {
itemRefs.current[index] = el;
}}
id={`${baseId}-btn-${item.id}`}
aria-haspopup="true"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={(event) => {
if (event.detail === 0) return;
focusPanelRef.current = false;
setOpenId(isOpen ? null : item.id);
}}
onKeyDown={(event) => handleTopKeyDown(event, index, item)}
onPointerEnter={(event) =>
handleItemPointerEnter(event, item)
}
className={`inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
isOpen
? "bg-slate-100 text-slate-900 dark:bg-white/10 dark:text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
}`}
>
{item.label}
<ChevronIcon open={isOpen} />
</button>
</li>
);
})}
</ul>
<div className="ml-auto flex items-center gap-1.5 sm:gap-2">
<a
href="#signin"
className="hidden rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white sm:inline-flex dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus-visible:ring-offset-slate-900"
>
Sign in
</a>
<a
href="#start"
className="inline-flex items-center gap-1.5 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 px-3.5 py-2 text-sm font-semibold text-white shadow-sm shadow-indigo-600/25 transition-transform hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
Start free
</a>
<button
type="button"
aria-label={mobileOpen ? "Close menu" : "Open menu"}
aria-haspopup="true"
aria-expanded={mobileOpen}
aria-controls={mobilePanelId}
onClick={() => setMobileOpen((prev) => !prev)}
className="grid h-9 w-9 place-items-center rounded-lg text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white md:hidden dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus-visible:ring-offset-slate-900"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-5 w-5"
>
{mobileOpen ? (
<path d="m6 6 12 12M18 6 6 18" />
) : (
<path d="M4 7h16M4 12h16M4 17h16" />
)}
</svg>
</button>
</div>
</div>
<AnimatePresence>
{activeMenu && activeMenu.panel && (
<motion.div
key="mega-panel"
ref={panelRef}
id={panelId}
role="region"
aria-label={`${activeMenu.label} menu`}
initial={panelVariants.initial}
animate={panelVariants.animate}
exit={panelVariants.exit}
transition={{ duration: reduced ? 0.12 : 0.2, ease: [0.22, 1, 0.36, 1] }}
onKeyDown={(event) => handlePanelKeyDown(event, activeIndex)}
className="absolute left-1/2 top-full z-50 mt-3 hidden w-[min(56rem,92vw)] -translate-x-1/2 md:block"
>
<div className="overflow-hidden rounded-2xl border border-slate-200/80 bg-white/95 p-2 shadow-2xl shadow-slate-900/10 backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/95 dark:shadow-black/50">
<div className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_1.15fr]">
{activeMenu.panel.groups.map((group) => {
const headingId = `${baseId}-${activeMenu.id}-${group.heading
.toLowerCase()
.replace(/[^a-z]+/g, "-")}`;
return (
<div key={group.heading} className="p-2">
<p
id={headingId}
className="px-2 pb-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400 dark:text-slate-500"
>
{group.heading}
</p>
<ul aria-labelledby={headingId} className="space-y-0.5">
{group.links.map((link) => (
<li key={link.href}>
<a
href={link.href}
onClick={(event) => {
event.preventDefault();
setOpenId(null);
}}
className="group/link flex gap-3 rounded-xl p-2 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:hover:bg-white/[0.06] dark:focus-visible:ring-offset-slate-900"
>
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-slate-100 text-slate-500 transition-colors group-hover/link:bg-indigo-500 group-hover/link:text-white dark:bg-white/[0.06] dark:text-slate-400 dark:group-hover/link:bg-indigo-500 dark:group-hover/link:text-white [&>svg]:h-5 [&>svg]:w-5">
{icons[link.icon]}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-slate-800 dark:text-slate-100">
{link.label}
</span>
<span className="mt-0.5 block text-xs leading-relaxed text-slate-500 dark:text-slate-400">
{link.description}
</span>
</span>
</a>
</li>
))}
</ul>
</div>
);
})}
<div className="p-2">
<a
href={activeMenu.panel.featured.href}
onClick={(event) => {
event.preventDefault();
setOpenId(null);
}}
className={`group/feat relative flex h-full flex-col justify-between overflow-hidden rounded-xl bg-gradient-to-br ${
toneGradient[activeMenu.panel.featured.tone]
} p-4 text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900`}
>
<span
aria-hidden="true"
className="cscd-glow pointer-events-none absolute -right-10 -top-12 h-40 w-40 rounded-full bg-white/25 blur-2xl"
/>
<span
aria-hidden="true"
className="cscd-sheen pointer-events-none absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/25 to-transparent"
/>
<div className="relative">
<span className="inline-flex items-center rounded-full bg-white/20 px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.14em]">
{activeMenu.panel.featured.eyebrow}
</span>
<h3 className="mt-3 text-base font-semibold leading-snug">
{activeMenu.panel.featured.title}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-white/85">
{activeMenu.panel.featured.body}
</p>
</div>
<span className="relative mt-4 inline-flex items-center gap-1.5 text-sm font-semibold">
{activeMenu.panel.featured.cta}
<ArrowIcon />
</span>
</a>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{mobileOpen && (
<motion.div
key="mobile-panel"
id={mobilePanelId}
initial={reduced ? { opacity: 0 } : { opacity: 0, height: 0 }}
animate={reduced ? { opacity: 1 } : { opacity: 1, height: "auto" }}
exit={reduced ? { opacity: 0 } : { opacity: 0, height: 0 }}
transition={{ duration: reduced ? 0.12 : 0.24, ease: [0.22, 1, 0.36, 1] }}
onKeyDown={(event) => {
if (event.key === "Escape") {
setMobileOpen(false);
}
}}
className="overflow-hidden md:hidden"
>
<div className="mt-2 rounded-2xl border border-slate-200/80 bg-white/95 p-2 shadow-lg shadow-slate-900/5 backdrop-blur-md dark:border-white/10 dark:bg-slate-900/90 dark:shadow-black/30">
{menus.map((item) => {
if (!item.panel) {
return (
<a
key={item.id}
href={item.href}
onClick={() => setMobileOpen(false)}
className="flex items-center justify-between rounded-xl px-3 py-3 text-sm font-semibold text-slate-800 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-100 dark:hover:bg-white/[0.06] dark:focus-visible:ring-offset-slate-900"
>
{item.label}
</a>
);
}
const expanded = mobileSection === item.id;
const sectionId = `${mobilePanelId}-${item.id}`;
return (
<div key={item.id}>
<button
type="button"
aria-expanded={expanded}
aria-controls={sectionId}
onClick={() =>
setMobileSection((prev) =>
prev === item.id ? null : item.id,
)
}
className="flex w-full items-center justify-between rounded-xl px-3 py-3 text-sm font-semibold text-slate-800 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-100 dark:hover:bg-white/[0.06] dark:focus-visible:ring-offset-slate-900"
>
{item.label}
<ChevronIcon open={expanded} />
</button>
{expanded && (
<div id={sectionId} className="px-1 pb-2">
{item.panel.groups.map((group) => (
<div key={group.heading} className="mb-1">
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400 dark:text-slate-500">
{group.heading}
</p>
<ul>
{group.links.map((link) => (
<li key={link.href}>
<a
href={link.href}
onClick={() => setMobileOpen(false)}
className="flex items-center gap-3 rounded-lg px-2 py-2.5 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:hover:bg-white/[0.06] dark:focus-visible:ring-offset-slate-900"
>
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-slate-100 text-slate-500 dark:bg-white/[0.06] dark:text-slate-400 [&>svg]:h-4 [&>svg]:w-4">
{icons[link.icon]}
</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">
{link.label}
</span>
</a>
</li>
))}
</ul>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
<header className="mx-auto mt-16 max-w-3xl text-center sm:mt-24">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
The workflow layer for data teams
</p>
<h1 className="mt-4 text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
One place to build, run, and watch every pipeline
</h1>
<p className="mx-auto mt-5 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
Hover or focus a menu item above to open its section. Use the arrow
keys to move between items, Enter to open, and Escape to close.
</p>
</header>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Dropdown Menu
Originalbasic dropdown menu with items
Dropdown Icons Menu
Originaldropdown with icons and shortcuts

User Menu
Originaluser account dropdown with avatar

Mega Cols Menu
Originalmulti-column mega menu with featured

Context Menu
Originalright-click context menu

Nested Menu
Originaldropdown with nested submenus

Select Menu
Originalcustom single select dropdown

Multiselect Menu
Originalmulti-select with checkable items

Command Menu
Originalcommand palette style menu with search

Actions Menu
Originalactions/kebab menu

Share Menu
Originalshare menu with options

Notifications Menu
Originalnotifications dropdown list

