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

React中移动端菜单打开时禁止页面垂直滚动的实现方案

移动端导航栏菜单联动滚动锁定修复方案

问题根源

原有代码存在4个核心问题导致功能异常:

  • 滚动锁定逻辑没有和菜单开关状态isOpen绑定,直接全局设置overflow: hidden导致页面永久锁死
  • 导航栏背景切换的滚动事件直接写在组件函数体内,每次组件重渲染都会重复绑定事件,既造成内存泄漏,也会干扰滚动逻辑
  • 滚动隐藏导航栏的逻辑没有做菜单状态拦截,就算触发异常滚动也会把导航栏连带菜单移出视口
  • iOS端仅设置overflow: hidden无法完全锁定触摸滚动,会出现滚动穿透问题

修复步骤

  • 新增独立的useEffect监听菜单开关状态,菜单打开时锁定页面滚动,关闭/组件卸载时还原滚动状态,同时记录滚动位置避免锁滚动后页面跳回顶部
  • 在导航栏滚动隐藏逻辑里加前置拦截,菜单打开时直接跳过隐藏判断
  • 把散落在组件函数体里的导航栏背景/阴影切换逻辑合并到统一的滚动处理函数中,删除重复绑定的滚动事件
  • 修正原有代码里boxShadow样式赋值的语法错误(之前多写了一层box-shadow:属性名前缀,导致样式不生效)

修复后完整代码

import * as React from 'react';
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Squash as Hamburger } from 'hamburger-react';

import { debounce } from './utilities/helpers';

const links = [
  {name: "Home", path: "/"},
  {name: "Projects", path: "/projects"},
  {name: "Contact", path: "/contact"}
];

export default function Header() {
  // 导航栏滚动隐藏状态
  const [prevScrollPos, setPrevScrollPos] = useState(0);
  const [visible, setVisible] = useState(true);
  // 汉堡菜单状态
  const [isOpen, setOpen] = useState(false);

  // 统一滚动处理逻辑
  const handleScroll = debounce(() => {
    // 菜单打开时跳过导航栏隐藏逻辑
    if (isOpen) return;

    const currentScrollPos = window.pageYOffset;
    // 导航栏显隐判断
    setVisible((prevScrollPos > currentScrollPos && prevScrollPos - currentScrollPos > 75) || currentScrollPos < 10);
    setPrevScrollPos(currentScrollPos);

    // 导航栏背景和阴影切换
    const navBar = document.getElementById('navbar');
    if(currentScrollPos >= 1){ 
      navBar.style.background = '#181a1d'; 
      navBar.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)';
    } else { 
      navBar.style.background = 'none'; 
      navBar.style.boxShadow = 'none'
    }
  }, 100);

  // 绑定导航栏滚动监听
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [prevScrollPos, visible, isOpen, handleScroll]);

  // 菜单打开时锁定页面滚动
  useEffect(() => {
    if (isOpen) {
      const scrollY = window.scrollY;
      // 兼容iOS滚动穿透问题
      document.body.style.position = 'fixed';
      document.body.style.top = `-${scrollY}px`;
      document.body.style.width = '100%';
      document.body.style.overflow = 'hidden';
    } else {
      const scrollY = document.body.style.top;
      // 还原页面样式
      document.body.style.position = '';
      document.body.style.top = '';
      document.body.style.width = '';
      document.body.style.overflow = '';
      // 还原原有滚动位置
      window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
    }
    // 组件卸载(路由跳转)时自动解除滚动锁定
    return () => {
      document.body.style.position = '';
      document.body.style.top = '';
      document.body.style.width = '';
      document.body.style.overflow = '';
    }
  }, [isOpen]);

  return (
    <nav style={{ top: visible ? '0' : '-75px' }} id="navbar" className="w-full h-[75px] fixed py-2 px-3 sm:px-6 flex justify-between items-center z-20 transition-all duration-300 ease-in-out">
      {/* Logo区域 */}
      <Link to="/">
        <div className="bg-[url('../public/Images/logo.png')] hover:bg-[url('../public/Images/logo-hover.png')] w-[260px] h-[60px]"></div>
      </Link>

      <div className="flex">
        {/* 汉堡按钮 */}
        <div className="lg:hidden mb-0.5 z-10">
          <Hamburger
            color="#ffffff"
            easing="ease-in-out"
            rounded
            toggled={isOpen}
            toggle={setOpen}
          />
        </div>

        {/* 移动端菜单内容 */}
        {isOpen && (
          <>
            <div className="h-screen w-screen absolute mt-[3.75rem] left-0 bg-black opacity-60 blur-sm"></div>
            
            <ul className="h-screen w-3/5 sm:w-1/2 md:w-2/5 bg-quaternary flex flex-col lg:flex-row items-center absolute top-0 right-0 pt-32">
              {links.map((link) => (
                <li key={link.path} className="m-5 lg:my-0 lg:mx-5 leading-10">
                  <Link 
                    className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out"
                    to={link.path} 
                    onClick={() => setOpen(false)}
                  >
                    {link.name}
                  </Link>
                </li>
              ))}
            </ul>
          </>
        )}

        {/* 端导航链接 */}
        <ul className= "hidden lg:flex flex-row">
          {links.map((link) => (
            <li key={link.path} className="m-5 lg:my-0 lg:mx-5">
              <Link className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out" to={link.path}>
                {link.name}
              </Link>
            </li>
          ))}
        </ul>
      </div>
    </nav>
  );
}

额外优化点

  • 给循环渲染的li元素补充了key属性,避免React渲染警告
  • 删除了冗余的ToggleNavLinks函数,该函数逻辑已经合并到滚动处理中,且汉堡组件本身已经绑定了开关状态
  • 给滚动监听的useEffect依赖数组补充了isOpen,保证状态变化时逻辑同步

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 09:15:35