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

React实现页面滚动时侧边栏目录自动高亮当前标题的方法咨询

实现滚动时自动高亮目录项的方案

结合你的现有代码,我们可以通过滚动监听+位置计算+状态管理来实现这个功能,以下是具体修改步骤:

1. 重构高亮逻辑为React状态管理

把原本直接操作DOM的高亮逻辑改成用React状态控制,避免DOM操作和React状态不一致的问题:

export function TableOfContents() {
  const ref = React.useRef()
  const headings = useTocHighlight(ref)
  // 新增状态:跟踪当前激活的标题索引
  const [activeIndex, setActiveIndex] = React.useState(null)

  const handleClickOnTopic = (e, index) => {
    e.preventDefault()
    setActiveIndex(index) // 点击时更新激活状态

    const nav = document.querySelector('.header')
    const height = nav.getBoundingClientRect().height - 50

    const target = e.currentTarget
    const splitted = target.href.split('/')
    const lastElement2 = splitted.pop()
    const topicId = document.querySelector(lastElement2)
    window.scrollTo(0, topicId.offsetTop - height)
  }

  // ...后续代码
}

然后在渲染<a>标签时,通过状态控制activeee类:

<a
  id={`heading${i}`}
  onClick={(e) => handleClickOnTopic(e, i)}
  href={heading.url}
  className={activeIndex === i ? 'activeee' : ''}
>
  {heading.text}
</a>

2. 添加滚动监听与位置计算逻辑

使用useEffect监听页面滚动,计算当前视口中的标题,自动更新激活状态:

React.useEffect(() => {
  // 防抖函数,避免滚动时频繁计算
  let timeoutId
  const handleScroll = () => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      const nav = document.querySelector('.header')
      const navHeight = nav.getBoundingClientRect().height - 50
      // 获取页面中所有H2、H3元素(需确保这些元素的ID和TOC的href对应)
      const pageHeadings = document.querySelectorAll('h2, h3')
      if (!pageHeadings.length) return

      // 遍历标题,找到当前处于视口内的第一个标题
      let currentActiveIndex = null
      pageHeadings.forEach((heading) => {
        const rect = heading.getBoundingClientRect()
        // 判断标题是否进入导航下方的有效区域(距离顶部navHeight的位置)
        if (rect.top <= navHeight + 20 && rect.bottom >= navHeight) {
          // 找到对应的TOC索引
          const matchingTocIndex = headings.findIndex(
            tocItem => tocItem.url.includes(`#${heading.id}`)
          )
          if (matchingTocIndex !== -1) {
            currentActiveIndex = matchingTocIndex
          }
        }
      })

      setActiveIndex(currentActiveIndex)
    }, 100) // 100ms防抖,可根据需求调整
  }

  // 添加滚动监听
  window.addEventListener('scroll', handleScroll)
  // 组件卸载时移除监听
  return () => {
    window.removeEventListener('scroll', handleScroll)
    clearTimeout(timeoutId)
  }
}, [headings]) // 依赖headings,确保目录更新时重新绑定逻辑

3. 关键注意事项

  • 确保页面中的H2/H3元素都有唯一ID,且TOC的heading.url包含对应的#id(比如#section-1),这样才能匹配到对应的目录项。
  • 防抖时间可根据体验调整,100ms左右既能保证性能又不会有明显延迟。
  • 若你的useTocHighlight钩子已经能正确获取H2/H3的信息,那上述逻辑可以直接对接;如果钩子返回的heading.url格式不对,需要调整matchingTocIndex里的匹配逻辑。

最终完整代码示例

export function TableOfContents() {
  const ref = React.useRef()
  const headings = useTocHighlight(ref)
  const [activeIndex, setActiveIndex] = React.useState(null)

  const handleClickOnTopic = (e, index) => {
    e.preventDefault()
    setActiveIndex(index)

    const nav = document.querySelector('.header')
    const height = nav.getBoundingClientRect().height - 50

    const target = e.currentTarget
    const splitted = target.href.split('/')
    const lastElement2 = splitted.pop()
    const topicId = document.querySelector(lastElement2)
    window.scrollTo(0, topicId.offsetTop - height)
  }

  React.useEffect(() => {
    let timeoutId
    const handleScroll = () => {
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => {
        const nav = document.querySelector('.header')
        const navHeight = nav.getBoundingClientRect().height - 50
        const pageHeadings = document.querySelectorAll('h2, h3')
        if (!pageHeadings.length) return

        let currentActiveIndex = null
        pageHeadings.forEach((heading) => {
          const rect = heading.getBoundingClientRect()
          if (rect.top <= navHeight + 20 && rect.bottom >= navHeight) {
            const matchingTocIndex = headings.findIndex(
              tocItem => tocItem.url.includes(`#${heading.id}`)
            )
            if (matchingTocIndex !== -1) {
              currentActiveIndex = matchingTocIndex
            }
          }
        })

        setActiveIndex(currentActiveIndex)
      }, 100)
    }

    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
      clearTimeout(timeoutId)
    }
  }, [headings])

  if (!headings.length) return null
  return (
    <TocContainer ref={ref}>
      <h4>On this page</h4>
      <ul>
        {headings.map((heading, i) =>
          heading.url ? (
            <li key={i} data-depth={heading.depth}>
              <a
                id={`heading${i}`}
                onClick={(e) => handleClickOnTopic(e, i)}
                href={heading.url}
                className={activeIndex === i ? 'activeee' : ''}
              >
                {heading.text}
              </a>
            </li>
          ) : null,
        )}
      </ul>
    </TocContainer>
  )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 02:40:30