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

React下拉菜单滚动时无法固定于触发按钮,求解决方案

React下拉菜单滚动时位置偏移问题解决方案

问题描述

在React中构建下拉菜单时,点击触发按钮打开菜单显示正常,但滚动页面时,菜单会随页面滚动,无法保持在触发按钮旁,不符合预期。

相关代码

import { createContext, useContext, useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { HiEllipsisVertical } from "react-icons/hi2";
import styled from "styled-components";

const Menu = styled.div`
  display: flex;
  align-items: center;
  justify-content: flex-end;
`;

const StyledToggle = styled.button`
  background: none;
  border: none;
  padding: 0.4rem;
  border-radius: var(--border-radius-sm);
  transform: translateX(0.8rem);
  transition: all 0.2s;

  &:hover {
    background-color: var(--color-grey-100);
  }

  & svg {
    width: 2.4rem;
    height: 2.4rem;
    color: var(--color-grey-700);
  }
`;

const StyledList = styled.ul`
  position: absolute; /* Should be relative to the button */
  background-color: var(--color-grey-0);
  box-shadow: var(--shadow-md);
  border-radius: var(--border-radius-md);

  left: ${(props) => props.$position.x}px;
  top: ${(props) => props.$position.y}px;
`;

const StyledButton = styled.button`
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 1.2rem 2.4rem;
  font-size: 1.4rem;
  transition: all 0.2s;

  display: flex;
  align-items: center;
  gap: 1.6rem;

  &:hover {
    background-color: var(--color-grey-50);
  }

  & svg {
    width: 1.6rem;
    height: 1.6rem;
    color: var(--color-grey-400);
    transition: all 0.3s;
  }
`;

const MenusContext = createContext();

function Menus({ children }) {
  const [openId, setOpenId] = useState("");
  const [position, setPosition] = useState(null);

  const close = () => setOpenId("");
  const open = (id) => setOpenId(id);

  return (
    <MenusContext.Provider value={{ openId, close, open, position, setPosition }}>
      {children}
    </MenusContext.Provider>
  );
}

function Toggle({ id }) {
  const { openId, close, open, setPosition } = useContext(MenusContext);

  function handleClick(e) {
    const rect = e.target.closest("button").getBoundingClientRect();
    setPosition({
      x: rect.x, // Position relative to the button's left corner
      y: rect.y + rect.height + 8, // Below the button
    });

    openId === "" || openId !== id ? open(id) : close();
  }

  return (
    <StyledToggle onClick={handleClick}>
      <HiEllipsisVertical />
    </StyledToggle>
  );
}

function List({ id, children }) {
  const { openId, position, setPosition } = useContext(MenusContext);

  useEffect(() => {
    function updatePosition() {
      const toggleButton = document.querySelector(`button[data-id="${id}"]`);
      if (toggleButton) {
        const rect = toggleButton.getBoundingClientRect();
        setPosition({
          x: rect.x,
          y: rect.y + rect.height + 8,
        });
      }
    }

    if (openId === id) {
      window.addEventListener("scroll", updatePosition);
      window.addEventListener("resize", updatePosition);
      updatePosition(); // Initial position update
    }

    return () => {
      window.removeEventListener("scroll", updatePosition);
      window.removeEventListener("resize", updatePosition);
    };
  }, [openId, id, setPosition]);

  if (openId !== id) return null;

  return createPortal(
    <StyledList $position={position}>{children}</StyledList>,
    document.body
  );
}

function Button({ children }) {
  return (
    <li>
      <StyledButton>{children}</StyledButton>
    </li>
  );
}

Menus.Menu = Menu;
Menus.Toggle = Toggle;
Menus.List = List;
Menus.Button = Button;

export default Menus;

问题

  1. 如何确保下拉菜单在页面滚动时仍固定于触发按钮旁?
  2. 在React中使用styled-components实现该行为,有哪些替代方法?

解决方案

问题1:修复滚动时菜单位置偏移

当前代码存在两个核心问题:触发按钮的选择器无效、位置坐标计算错误。

步骤1:给触发按钮添加标识属性

List组件中通过button[data-id="${id}"]查找按钮,但Toggle组件的按钮未设置该属性,导致无法获取按钮元素更新位置。修改Toggle组件:

function Toggle({ id }) {
  // ... 其他代码
  return (
    <StyledToggle onClick={handleClick} data-id={id}>
      <HiEllipsisVertical />
    </StyledToggle>
  );
}

步骤2:修正坐标计算逻辑

getBoundingClientRect()返回的是元素相对于视口的位置,而挂载在document.body上的position: absolute元素是相对于文档定位的。需要将视口坐标转换为文档坐标,加上页面滚动偏移量:

// Toggle组件的handleClick函数
setPosition({
  x: rect.x + window.scrollX,
  y: rect.y + rect.height + 8 + window.scrollY,
});

// List组件的updatePosition函数
setPosition({
  x: rect.x + window.scrollX,
  y: rect.y + rect.height + 8 + window.scrollY,
});

步骤3:验证事件监听

修复上述问题后,scroll和resize事件会正确触发位置更新,菜单将随按钮滚动保持相对位置。


问题2:styled-components实现该行为的替代方法

方法1:使用position: fixed定位

将菜单的position改为fixed,直接使用视口坐标(无需添加滚动偏移量),因为fixed元素始终相对于视口定位,滚动时自动保持位置:

const StyledList = styled.ul`
  position: fixed;
  background-color: var(--color-grey-0);
  box-shadow: var(--shadow-md);
  border-radius: var(--border-radius-md);

  left: ${(props) => props.$position.x}px;
  top: ${(props) => props.$position.y}px;
`;

注意:需额外处理菜单超出视口的边界问题。

方法2:取消Portal,利用父元素相对定位

将菜单放在触发按钮的父元素内,给父元素设置position: relative,菜单使用position: absolute即可自动相对于父元素定位,无需手动计算坐标:

// 修改Menu组件的样式
const Menu = styled.div`
  display: flex;
  align-items: center;
  justify-content: flex-end;
  position: relative; /* 添加相对定位 */
`;

// 修改List组件,去掉Portal和位置计算
function List({ id, children }) {
  const { openId } = useContext(MenusContext);
  if (openId !== id) return null;
  return <StyledList>{children}</StyledList>;
}

优点:无需手动处理坐标和滚动事件,代码更简洁;缺点:菜单会受父元素溢出隐藏的影响。

方法3:集成Popper.js

使用专门处理浮动元素定位的库(如@popperjs/core),自动处理滚动、边界检测等场景,结合styled-components使用:

// 安装依赖:npm install @popperjs/core
import { createPopper } from '@popperjs/core';
import { useRef } from 'react';

function List({ id, children }) {
  const { openId } = useContext(MenusContext);
  const [popperElement, setPopperElement] = useState(null);
  const toggleRef = useRef(null);

  useEffect(() => {
    if (openId === id && toggleRef.current && popperElement) {
      const popper = createPopper(toggleRef.current, popperElement, {
        placement: 'bottom-start', // 设置菜单位置
      });
      return () => popper.destroy();
    }
  }, [openId, id]);

  if (openId !== id) return null;
  return (
    <StyledList ref={setPopperElement}>{children}</StyledList>
  );
}

// 在Toggle组件中添加ref
function Toggle({ id, ref }) {
  // ... 其他代码
  return (
    <StyledToggle ref={ref} onClick={handleClick} data-id={id}>
      <HiEllipsisVertical />
    </StyledToggle>
  );
}

优点:专业处理各种复杂场景(如滚动、视口边界、动态元素),无需手动维护位置逻辑。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 20:05:57