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

如何实现React+TailwindCSS导航菜单鼠标悬停展开功能?

鼠标悬停展开导航菜单的问题修复与优化方案

我是React与Next.js学习第一周的新手,需要实现一个仅在鼠标悬停时展开的导航菜单,技术栈为React+TailwindCSS。尝试过多种教程后始终无法实现预期效果:要么菜单一直展开无法关闭,要么默认关闭后无法通过悬停触发展开。以下是我编写的三个核心组件代码:

原组件代码

'use client';

import React, { useState } from 'react';
import { FaBars } from 'react-icons/fa';
import { HiChevronDown } from 'react-icons/hi';

interface MenuItemProps {
  label: string;
  link: string;
  onMouseEnter?: () => void;
  onMouseLeave?: () => void;
  onClick?: () => void;
}

const MenuItem: React.FC<MenuItemProps> = ({
  label,
  onMouseEnter,
  onMouseLeave,
  onClick,
}) => {
  const [isOpen, setIsOpen] = useState(false);

  const handleMouseEnter = () => {
    setIsOpen(true);
    if (onMouseEnter) {
      onMouseEnter();
    }
  };

  const handleMouseLeave = () => {
    setIsOpen(false);
    if (onMouseLeave) {
      onMouseLeave();
    }
  };

  return (
    <nav>
      <div
        className="px-6 py-3 hover:bg-neutral-100 hover:rounded-xl transition font-semibold cursor-pointer"
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        onClick={onClick}
      >
        <span className="flex items-center">
          {label}{' '}
          <HiChevronDown
            className={`ml-1 ${isOpen ? 'transform rotate-180' : ''}`}
            style={{ marginTop: '0.125rem' }}
          />
        </span>
        {isOpen && (
          <ul className="pl-6 mt-2">
            <li>Item 1</li>
            <li>Item 2</li>
            <li>Item 3</li>
            {/* Add more menu items here */}
          </ul>
        )}
      </div>
    </nav>
  );
};

export default MenuItem;

UserMenu组件

'use client';

import React, { useState } from "react";
import { AiOutlineMenu } from "react-icons/ai";
import { signOut } from "next-auth/react";
import { useRouter } from "next/router";

import MenuItem from "./menuItem";

interface UserMenuProps {
  onClick?: () => void;
  label: string;
  link: string;
}

const UserMenu: React.FC<UserMenuProps> = ({ onClick, label, link }) => {
  const [menuIsOpen, setMenuIsOpen] = useState(false);

  const handleToggle = () => {
    setMenuIsOpen((prevOpen) => !prevOpen);
  };

  return (
    <div className="relative items-center gap-7">
      <div>
        <div onClick={handleToggle}>
          <MenuItem label={label} onClick={onClick} link="" />
        </div>
        {menuIsOpen && (
          <ul className="pl-6 mt-2">
            <li>
              <a href={link}>{label}</a>
            </li>
          </ul>
        )}
      </div>
    </div>
  );
};

export default UserMenu;
'use client';

import React, { ReactElement, useState } from 'react';
import { FaArrowDown } from 'react-icons/fa';
import { useCallback } from 'react';

import Container from '../contanier';
import Logo from './logo';
import MenuItem from './menuItem';
import UserMenu from './userMenu';

interface MegaMenuItemProps{
  label: string,
  link: string,
  children: ReactElement
}

const MegaMenuItem = ({ label, link, children }) => {
  const [isHovered, setIsHovered] = useState(false);

  const handleMouseEnter = () => {
    setIsHovered(true);
  };

  const handleMouseLeave = () => {
    setIsHovered(false);
  };
  const toggleOpen = useCallback(() => {
    setIsHovered((value) => !value);
  }, []);

  return (
    <div
      className="relative"
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      <UserMenu label={label} link={link} onClick={toggleOpen} />
      {isHovered && (
        <div className="absolute left-0 top-full pl-2 mt-1 bg-white shadow-md">
          <MenuItem label={'label'} link={'/'}/>
          <MenuItem label={'label'} link={'/'}/>
          <MenuItem label={'label'} link={'/'}/>
        </div>
      )}
    </div>
  );
};

const NavBar = () => {
  return (
    <div>
      <Container>
        <div className="flex flex-auto bg-white sticky items-center justify-between gap-7 md:gap-1 p-5">
          <Logo />
          <div>
            <MegaMenuItem label="product" link="/">
            </MegaMenuItem>
          </div>
          <div className="space-x-4">
            <button className="font-semibold bg-amber-400 rounded-full p-3 justify-center text-white hover:bg-amber-600 ">
              <a href="/signup">Sign Up</a>
            </button>
            <button className="font-semibold bg-white rounded-full p-3 justify-center text-amber-600 hover:bg-amber-400 hover:text-white hover:shadow-sm">
              <a href="/login">Log In</a>
            </button>
          </div>
        </div>
      </Container>
    </div>
  );
};

export default NavBar;

问题核心分析

  1. 组件层级冗余+状态分散:三个组件各自维护独立的isOpen/menuIsOpen/isHovered状态,hover事件在多层传递时互相干扰,导致状态逻辑混乱
  2. 交互逻辑冲突:UserMenu中加入了点击切换逻辑,与hover展开的需求冲突,进一步打乱状态控制
  3. hover范围不合理:MegaMenuItem的hover区域仅包含触发按钮,鼠标移到子菜单时会触发onMouseLeave导致菜单关闭

修复方案与优化代码

简化后的MenuItem组件

仅负责渲染单个菜单项,移除冗余状态和事件

'use client';

import React from 'react';
import { HiChevronDown } from 'react-icons/hi';

interface MenuItemProps {
  label: string;
  link?: string;
  hasChildren?: boolean;
  isOpen?: boolean;
}

const MenuItem: React.FC<MenuItemProps> = ({
  label,
  link,
  hasChildren = false,
  isOpen = false,
}) => {
  return (
    <div className="px-6 py-3 font-semibold cursor-pointer hover:bg-neutral-100 rounded-xl transition w-full">
      <span className="flex items-center">
        {link ? <a href={link} className="w-full">{label}</a> : label}
        {hasChildren && (
          <HiChevronDown
            className={`ml-1 transition-transform ${isOpen ? 'rotate-180' : ''}`}
            style={{ marginTop: '0.125rem' }}
          />
        )}
      </span>
    </div>
  );
};

export default MenuItem;

合并后的MegaMenuItem组件

统一管理hover状态,确保鼠标移到子菜单时保持展开

'use client';

import React, { useState } from 'react';
import MenuItem from './menuItem';

interface MegaMenuItemProps {
  label: string;
  children?: React.ReactNode;
}

const MegaMenuItem: React.FC<MegaMenuItemProps> = ({ label, children }) => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div className="relative">
      {/* 触发hover的按钮 */}
      <div
        onMouseEnter={() => setIsHovered(true)}
        className="cursor-pointer"
      >
        <MenuItem label={label} hasChildren isOpen={isHovered} />
      </div>

      {/* 子菜单容器 - 鼠标移入时保持展开 */}
      {isHovered && (
        <div
          className="absolute left-0 top-full mt-1 bg-white shadow-md rounded-lg p-2 min-w-[200px]"
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
        >
          {children}
        </div>
      )}
    </div>
  );
};

export default MegaMenuItem;

修复后的NavBar组件

简化结构,使用优化后的组件实现功能

'use client';

import React from 'react';

import Container from '../contanier';
import Logo from './logo';
import MenuItem from './menuItem';
import MegaMenuItem from './megaMenuItem';

const NavBar = () => {
  return (
    <div>
      <Container>
        <div className="flex items-center justify-between gap-7 bg-white sticky p-5">
          <Logo />
          <div>
            <MegaMenuItem label="Product">
              <MenuItem label="Item 1" link="/item1" />
              <MenuItem label="Item 2" link="/item2" />
              <MenuItem label="Item 3" link="/item3" />
            </MegaMenuItem>
          </div>
          <div className="space-x-4">
            <button className="font-semibold bg-amber-400 rounded-full p-3 text-white hover:bg-amber-600">
              <a href="/signup">Sign Up</a>
            </button>
            <button className="font-semibold bg-white rounded-full p-3 text-amber-600 hover:bg-amber-400 hover:text-white hover:shadow-sm">
              <a href="/login">Log In</a>
            </button>
          </div>
        </div>
      </Container>
    </div>
  );
};

export default NavBar;

学习建议

  • 从简单到复杂:先实现单个hover展开的菜单项,再逐步扩展成多级菜单,避免一开始就嵌套多层组件
  • 状态集中管理:同一交互逻辑的状态尽量放在最外层父组件中,避免多个子组件各自维护状态导致冲突
  • 理解事件传递:React合成事件会冒泡,多层组件的onMouseEnter/onMouseLeave会互相影响,调试时可通过console.log跟踪状态变化
  • Tailwind配合状态:利用Tailwind的transition类实现平滑动画,通过状态控制类名切换(如rotate-180)
  • 使用React DevTools:查看组件状态变化,快速定位状态异常的原因

内容的提问来源于stack exchange,提问作者abdul

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 08:27:08