# 超级菜单 Mega Menu > 鼠标移到顶部导航的某一项,下面展开一整块多列面板,还能放推荐图。 分类:导航 [全部组件和流程](https://www.dachuiai.com/llms.txt) · [导航选型规则](https://www.dachuiai.com/guides/navigation.md) ## 给 AI:用户把这个组件发给了你 1. 先看用户的项目:框架、页面类型、现在的风格、手机还是电脑为主。 2. 对照下面的「适合 / 不适合」判断合不合适。不合适就直接说,并从[导航选型规则](https://www.dachuiai.com/guides/navigation.md)或[全部组件](https://www.dachuiai.com/llms.txt)里另挑一个。 3. 合适就按「装进项目」装好,示例里的文字和图片换成用户自己的。 4. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:分类很多的电商和品牌官网:一个入口下面有十几个子分类,要一眼看全。 - 不适合:分类少(五六个以内用普通下拉菜单);手机(换成汉堡菜单);后台(用侧边栏)。 - 适合的网站:电商、官网、SaaS、内容站 - 放在:导航 - 手机上:要改:手机上没有悬停,也放不下多列,换成汉堡菜单里的分组列表。 - 性能代价:低:只在打开时渲染面板。 - 常搭配:[顶部导航栏 Navbar](https://www.dachuiai.com/c/navbar.md)、[吸顶导航 Sticky Header](https://www.dachuiai.com/c/sticky-header.md)、[汉堡菜单 Hamburger Menu](https://www.dachuiai.com/c/hamburger-menu.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react` - 文件:原样存成 `mega-menu.tsx`(比如 `src/components/mega-menu.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/mega-menu.tsx),比手抄准:`curl -fsSL -o src/components/mega-menu.tsx https://www.dachuiai.com/code/mega-menu.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`MegaMenu` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 图片:示例用了本站的 [/tex/s5.jpg](https://www.dachuiai.com/tex/s5.jpg)、[/tex/s1.jpg](https://www.dachuiai.com/tex/s1.jpg)、[/tex/s4.jpg](https://www.dachuiai.com/tex/s4.jpg)、[/tex/s6.jpg](https://www.dachuiai.com/tex/s6.jpg)、[/tex/s2.jpg](https://www.dachuiai.com/tex/s2.jpg)。换成用户自己的图;只想先看效果,可以从本站同样的路径下载,放进项目的 public 目录。 - 注意:MegaMenu 不收参数,分类、链接和推荐图都写在文件里,直接改文件里的数据。悬停有打开和关闭的延迟,鼠标斜着移进面板不会误关,改的时候保留。 - 保留代码里 `prefers-reduced-motion` 的处理:用户开了「减弱动态效果」时内容照样完整显示。 - Next.js(有 app 文件夹):文件第一行加 `'use client';`。 - 用 JavaScript 不用 TypeScript:Vite 和 Next.js 都能直接用 .tsx 文件;也可以删掉类型标注,改名 .jsx。 - 没有 Tailwind:先装 Tailwind CSS v4(Vite 项目 `npm i tailwindcss @tailwindcss/vite`,vite 配置的 plugins 里加 `tailwindcss()`,入口引入的那个 CSS 文件(一般是 `src/index.css`)第一行写 `@import "tailwindcss";`),或者把类名改写成项目用的样式。 - 已经装了 Tailwind v3:别为了一个组件升级。类名大多通用,少数 v4 写法要换,比如焦点框 `outline-2` 在 v3 里要写成 `outline outline-2`。 - 不是 React(Vue、Svelte、小程序、纯 HTML):照着代码的做法改写,时长、曲线、参数保持一样。Wix、有赞这类建站平台装不进代码,告诉用户去平台自带的动效设置里找。 ## 你可以说 > 顶部导航的「装备」做成 Mega Menu 超级菜单:鼠标移上去展开一整块面板,分四列放分类,右边放一张推荐商品图;分类少的「品牌」用普通下拉菜单。 ## 完整代码 ```tsx import {useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type ComponentType} from 'react'; import { ArrowRight, Backpack, BedDouble, ChevronDown, Flame, Lamp, MapPin, Mountain, Search, ShoppingBag, Tent, Trees, } from 'lucide-react'; /* --------------------------------------------------------------- * MegaMenu 超级菜单 * 鼠标移到顶部导航的某一项,下面展开一整块多列面板,还能放推荐图。 * 分类少的项(这里的「品牌」)用普通下拉菜单就够了。 * 支持:悬停延迟打开/关闭、点击、键盘(Enter / ↓ / Esc)、屏幕阅读器。 * ------------------------------------------------------------- */ type Icon = ComponentType<{size?: number; strokeWidth?: number; className?: string}>; type Column = {title: string; icon: Icon; links: string[]; hot?: string}; type Promo = {tag: string; title: string; price: string; img: string; icon: Icon}; type Item = | {label: string; kind: 'mega'; columns: Column[]; promo: Promo; all: string} | {label: string; kind: 'dropdown'; links: string[]} | {label: string; kind: 'link'}; const MENU: Item[] = [ { label: '装备', kind: 'mega', all: '全部装备', columns: [ {title: '帐篷', icon: Tent, links: ['双人帐篷', '家庭帐篷', '天幕', '地席'], hot: '双人帐篷'}, {title: '睡眠', icon: BedDouble, links: ['羽绒睡袋', '棉睡袋', '充气睡垫']}, {title: '背包', icon: Backpack, links: ['登山包', '徒步腰包', '防水袋']}, {title: '炉具', icon: Flame, links: ['卡式炉', '户外套锅', '咖啡壶']}, ], promo: {tag: '新品', title: '轻量双人帐篷', price: '¥899', img: '/tex/s5.jpg', icon: Tent}, }, { label: '露营', kind: 'mega', all: '全部露营', columns: [ {title: '营地', icon: MapPin, links: ['湖边营地', '森林营地', '海边营地'], hot: '湖边营地'}, {title: '照明', icon: Lamp, links: ['露营灯', '头灯', '氛围灯串']}, {title: '桌椅', icon: Trees, links: ['折叠桌', '月亮椅', '蛋卷桌']}, {title: '路线', icon: Mountain, links: ['周末两日', '亲子入门', '秋季赏叶']}, ], promo: {tag: '本周', title: '湖边营地 2 晚', price: '¥360 起', img: '/tex/s1.jpg', icon: Trees}, }, {label: '徒步', kind: 'dropdown', links: ['登山鞋', '登山杖', '速干衣']}, {label: '品牌', kind: 'dropdown', links: ['Nova 户外', 'Atlas 装备', 'Kite 露营']}, {label: '攻略', kind: 'link'}, ]; const OPEN_DELAY = 80; // 鼠标划过时不立刻弹出,避免误触 const CLOSE_DELAY = 160; // 鼠标移到面板的路上不会关掉 export function MegaMenu() { const [open, setOpen] = useState(null); const [hover, setHover] = useState(null); const timer = useRef(0); const triggers = useRef<(HTMLElement | null)[]>([]); const navRef = useRef(null); const [bar, setBar] = useState({x: 0, w: 0, on: false}); const uid = useId(); const schedule = (fn: () => void, ms: number) => { window.clearTimeout(timer.current); timer.current = window.setTimeout(fn, ms); }; const openNow = useCallback((i: number | null) => { window.clearTimeout(timer.current); setOpen(i); }, []); // 下划线跟着悬停/展开的那一项滑动 const lit = hover ?? open; useLayoutEffect(() => { const el = lit == null ? null : triggers.current[lit]; if (!el || !navRef.current) return setBar((b) => ({...b, on: false})); const a = el.getBoundingClientRect(), n = navRef.current.getBoundingClientRect(); setBar({x: a.left - n.left + 14, w: a.width - 28, on: true}); }, [lit]); useEffect(() => () => window.clearTimeout(timer.current), []); // 用 ↓ 打开时,把焦点送进面板的第一个链接 const focusFirst = useRef(false); useEffect(() => { if (open == null || !focusFirst.current) return; focusFirst.current = false; document.getElementById(`${uid}-panel-${open}`)?.querySelector('a')?.focus(); }, [open, uid]); const onKey = (e: React.KeyboardEvent, i: number) => { if (e.key === 'ArrowDown') { e.preventDefault(); focusFirst.current = true; openNow(i); } }; const onPanelKey = (e: React.KeyboardEvent) => { if (e.key === 'Escape' && open != null) { triggers.current[open]?.focus(); openNow(null); } }; const current = open == null ? null : MENU[open]; return (
schedule(() => openNow(null), CLOSE_DELAY)} onKeyDown={onPanelKey} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && openNow(null)} >
山野
{/* 大面板:多列 + 推荐图 */} {current?.kind === 'mega' && (
window.clearTimeout(timer.current)} >
)} {/* 普通下拉菜单:分类少时用它 */} {current?.kind === 'dropdown' && open != null && ( window.clearTimeout(timer.current)} /> )} {/* 打开大面板时,压暗下面的页面 */}
); } function DropdownPanel({id, anchor, host, links, onEnter}: {id: string; anchor: HTMLElement | null; host: HTMLElement | null; links: string[]; onEnter: () => void}) { const left = anchor && host ? anchor.getBoundingClientRect().left - host.getBoundingClientRect().left : 0; return (
{links.map((l) => ( {l} ))}
); } const MEGA_CSS = ` .mm-bar { transition: transform .28s cubic-bezier(.22,1,.36,1), width .28s cubic-bezier(.22,1,.36,1), opacity .2s; } .mm-panel { animation: mm-in .22s cubic-bezier(.22,1,.36,1); transform-origin: 50% 0; } .mm-col { animation: mm-fade .28s cubic-bezier(.22,1,.36,1) both; } @keyframes mm-in { from { opacity: 0; transform: translateY(-6px) scale(.985); } } @keyframes mm-fade { from { opacity: 0; transform: translateY(6px); } } @media (prefers-reduced-motion: reduce) { .mm-bar, .mm-panel, .mm-col { transition: none; animation: none; } } `; /* ---------------- 示例:「山野」户外店首页 ---------------- */ export default function MegaMenuDemo() { return (

去山里,过个周末

秋季露营装备 新品上架

去逛逛
{[['轻量双人帐篷', '¥899', '/tex/s5.jpg', Tent], ['徒步背包 40L', '¥459', '/tex/s6.jpg', Backpack], ['卡式炉', '¥239', '/tex/s2.jpg', Flame]].map(([n, p, img, I]) => { const Ic = I as Icon; return (
{n as string}
{p as string}
); })}
); } ```