如何用React+Headless UI(Tailwind)实现多层叠加滑动菜单?
实现可叠加滑入的层级滑动菜单
核心需求:
- 实现带子菜单的滑动导航,子菜单支持叠加滑入
- 子菜单提供「返回」按钮,点击可回到上一级菜单
问题分析
当前实现用独立的SlidePanel和SlidePanelLayer组件,通过单一openLayer状态控制子菜单显示,这种方式无法支持多级嵌套,且叠加动画逻辑割裂,导致效果不符合预期。更合理的方式是用菜单层级栈来管理导航状态,统一渲染逻辑。
解决方案代码
"use client"; import { Fragment, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { XMarkIcon, ChevronLeftIcon } from "@heroicons/react/24/outline"; import { menuItems } from "../data/menu"; export default function SlidePanel({ slideOpen, setSlideOpen }) { // 用栈存储菜单层级,初始为[主菜单] const [menuStack, setMenuStack] = useState([menuItems]); // 进入子菜单 const enterSubMenu = (subItems) => { setMenuStack([...menuStack, subItems]); }; // 返回上一级菜单 const goBack = () => { setMenuStack(prev => prev.slice(0, -1)); }; // 获取当前显示的菜单 const currentMenu = menuStack[menuStack.length - 1]; return ( <Transition.Root show={slideOpen} as={Fragment}> <Dialog as="div" className="relative z-50" onClose={setSlideOpen}> <Transition.Child as={Fragment} enter="ease-in-out duration-500" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in-out duration-500" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> </Transition.Child> <div className="fixed inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden"> {/* 为每个菜单层级创建独立的滑动面板,实现叠加效果 */} {menuStack.map((_, index) => ( <Transition.Child key={index} as={Fragment} enter="transform transition ease-in-out duration-500 sm:duration-700" enterFrom="translate-x-full" enterTo="translate-x-0" leave="transform transition ease-in-out duration-500 sm:duration-700" leaveFrom="translate-x-0" leaveTo="translate-x-full" show={index === menuStack.length - 1} > <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10"> <Dialog.Panel className="pointer-events-auto w-screen max-w-md"> <div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl rounded-l-2xl"> <div className="px-4 sm:px-6"> <div className="flex items-start justify-between"> <div className="flex h-7 items-center"> {/* 根据层级显示返回或关闭按钮 */} {menuStack.length > 1 ? ( <button type="button" className="flex gap-2 items-center relative rounded-md bg-white text-gray-400 hover:text-gray-500" onClick={goBack} > <ChevronLeftIcon className="h-5 w-5 text-secondary-90" aria-hidden="true" /> <span className="text-gray-900 font-bold">返回</span> </button> ) : ( <button type="button" className="flex gap-2 items-center relative rounded-md bg-white text-gray-400 hover:text-gray-500" onClick={() => setSlideOpen(false)} > <XMarkIcon className="h-5 w-5 text-secondary-90" aria-hidden="true" /> <span className="text-gray-900 font-bold">关闭</span> </button> )} </div> </div> </div> <div className="relative mt-6 flex-1 px-4 sm:px-6"> <div className="mt-6 flow-root"> <div className="-my-6"> <div className="space-y-2 pt-6 pb-4"> {currentMenu.map((item) => ( <button key={item.title} onClick={() => { // 如果有子菜单则进入,否则执行跳转逻辑 if (item.subItems) { enterSubMenu(item.subItems); } else { setSlideOpen(false); // 这里添加跳转逻辑,比如window.location.href = item.href; } }} className="-mx-3 block rounded-lg px-3 py-2 text-base leading-7 text-secondary-90 hover:bg-gray-50 font-bold flex justify-between items-center" > <span>{item.title}</span> {/* 有子菜单时显示箭头 */} {item.subItems && <ChevronLeftIcon className="h-5 w-5 text-secondary-90 rotate-180" aria-hidden="true" />} </button> ))} </div> </div> </div> </div> </div> </Dialog.Panel> </div> </Transition.Child> ))} </div> </div> </Dialog> </Transition.Root> ); }
关键改进点
- 层级栈管理:用
menuStack数组记录导航路径,支持无限级子菜单嵌套 - 统一渲染逻辑:单个组件内根据栈的层级渲染对应菜单,避免组件拆分导致的状态不同步
- 叠加动画实现:为每个层级创建独立的滑动面板,通过
show控制当前显示的层级,实现叠加滑入效果 - 交互优化:自动根据层级切换返回/关闭按钮,带子菜单的项显示箭头提示
配套菜单数据结构示例
确保你的menuItems包含子菜单字段,比如:
export const menuItems = [ { title: "首页", href: "/" }, { title: "产品", subItems: [ { title: "产品A", href: "/product-a" }, { title: "产品B", href: "/product-b" }, { title: "产品系列", subItems: [ { title: "系列1", href: "/series-1" }, { title: "系列2", href: "/series-2" } ] } ] }, { title: "关于我们", href: "/about" } ];
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

