You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 12:19:55