Web InnoventixFreeCode

Mega Menu

Original · free

mega menu with grouped links

byWeb InnoventixReact + Tailwind
menumegamenus
Open

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.json
menu-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 quote

Similar components

Browse all →