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

如何在React网站中实现侧边滚动章节追踪功能?

问题描述

我需要为我的网站实现一个右侧侧边章节追踪功能,该功能要能追踪用户当前所处的页面章节,并自动切换对应样式。目前没找到类似实现案例,考虑使用react-spring/Parallax但不清楚具体实现方式。相关设计图如下:
章节追踪侧边栏设计图

我已提供项目的App.js代码供参考:

import "./App.css";

import Navbar from "./components/navbar/navbar.component";
import Hero from "./components/hero/hero.component";
import Content from "./components/content/content.component";
import Footer from "./components/footer/footer.component";

import image from './components/content/01.png';
import image2 from './components/content/02.png';
import image3 from './components/content/03.png';


function App() {

  const reverseContainer = {
    direction: "rtl",
  };

  const textBeforeStyle = {
    left: '9.8rem',
  };

  const textStyle = {
    direction: "ltr",
  }


  const pictureStyle = {
    margin: 'auto auto auto -9rem',
  }

  return (
    <div className="App">
      <div className="navbar-container">
        <Navbar />
      </div>
      <Hero />
      <div className="all-content">
        <Content
          textBefore="01"
          textAbove="GEt Started"
          header="What level of hiker are you?"
          mainText="Determining what level of hiker you are can be an important tool when planning future
      hikes. This hiking level guide will help you plan hikes according to different hike
      ratings set by various websites like All Trails and Modern Hiker. What type of hiker
              are you – novice, moderate, advanced moderate, expert, or expert backpacker?"
          image={image}
          side1="right"
          side2="left"
        />
        <Content
          textBefore="02"
          textAbove="Hiking Essentials"
          header="Picking the right Hiking Gear!"
          mainText="The nice thing about beginning hiking is that you don’t really need any special gear, you can probably get away with things you already have.
    Let’s start with clothing. A typical mistake hiking beginners make is wearing jeans and regular clothes, which will get heavy and chafe wif they get sweaty or wet."
          image={image2}
          side1="right"
          side2="left"
          reverseContainer={reverseContainer}
          textBeforeStyle={textBeforeStyle}
          textStyle={textStyle}
          pictureStyle={pictureStyle}
        />
        <Content

          textBefore="03"
          textAbove="where you go is the key"
          header="Understand Your Map & Timing"
          mainText="To start, print out the hiking guide and map. If it’s raining, throw them in a Zip-Lock bag. Read over the guide, study the map, and have a good idea of what to expect. I like to know what my next landmark is as I hike. For example, I’ll read the guide and know that say, in a mile, I make a right turn at the junction.."
          image={image3}
          side1="right"
          side2="left"

        />
        <Footer />
      </div>
    </div>
  );
}

export default App;

实现建议

核心思路

不用依赖react-spring/Parallax也能实现基础的章节追踪,若要加动画再结合react-spring即可。整体分为三个步骤:标记章节节点、监听滚动判断活跃章节、渲染侧边导航并切换样式。


步骤1:给章节添加标识与引用

修改App.js,给每个Content组件添加唯一id,并用useRef存储所有章节的DOM引用,方便后续计算位置:

import "./App.css";
import { useState, useEffect, useRef } from "react"; 
import Navbar from "./components/navbar/navbar.component";
import Hero from "./components/hero/hero.component";
import Content from "./components/content/content.component";
import Footer from "./components/footer/footer.component";

import image from './components/content/01.png';
import image2 from './components/content/02.png';
import image3 from './components/content/03.png';

// 侧边追踪组件
const SidebarTracker = ({ activeChapter, chapters }) => {
  return (
    <div className="sidebar-tracker" style={{
      position: 'fixed',
      right: '2rem',
      top: '50%',
      transform: 'translateY(-50%)',
      display: 'flex',
      flexDirection: 'column',
      gap: '1.5rem'
    }}>
      {chapters.map((chapter, index) => (
        <button
          key={index}
          className={`tracker-item ${activeChapter === index ? 'active' : ''}`}
          onClick={() => {
            document.getElementById(chapter.id).scrollIntoView({ behavior: 'smooth' });
          }}
          style={{
            border: 'none',
            background: 'transparent',
            color: activeChapter === index ? '#2c3e50' : '#95a5a6',
            fontSize: '1rem',
            cursor: 'pointer',
            transition: 'all 0.3s ease',
            fontWeight: activeChapter === index ? 700 : 400,
            borderLeft: activeChapter === index ? '3px solid #2c3e50' : '3px solid transparent',
            paddingLeft: '0.5rem'
          }}
        >
          {chapter.label}
        </button>
      ))}
    </div>
  );
};

function App() {
  // 章节数据:id对应Content的id,label是侧边显示的文字
  const chapters = [
    { id: 'chapter-1', label: '01 入门指南' },
    { id: 'chapter-2', label: '02 装备选择' },
    { id: 'chapter-3', label: '03 地图与时机' }
  ];
  const [activeChapter, setActiveChapter] = useState(0);
  const chapterRefs = useRef([]); 

  const reverseContainer = {
    direction: "rtl",
  };

  const textBeforeStyle = {
    left: '9.8rem',
  };

  const textStyle = {
    direction: "ltr",
  }

  const pictureStyle = {
    margin: 'auto auto auto -9rem',
  }

  // 监听滚动,判断当前活跃章节
  useEffect(() => {
    const handleScroll = () => {
      const scrollPosition = window.scrollY + window.innerHeight / 2; 
      
      chapterRefs.current.forEach((ref, index) => {
        if (!ref) return;
        const offsetTop = ref.offsetTop;
        const offsetBottom = offsetTop + ref.offsetHeight;
        
        if (scrollPosition >= offsetTop && scrollPosition < offsetBottom) {
          setActiveChapter(index);
        }
      });
    };

    window.addEventListener('scroll', handleScroll);
    handleScroll();

    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <div className="App">
      <div className="navbar-container">
        <Navbar />
      </div>
      <Hero />
      {/* 渲染侧边追踪组件 */}
      <SidebarTracker activeChapter={activeChapter} chapters={chapters} />
      <div className="all-content">
        <Content
          id="chapter-1" 
          ref={(el) => (chapterRefs.current[0] = el)} 
          textBefore="01"
          textAbove="GEt Started"
          header="What level of hiker are you?"
          mainText="Determining what level of hiker you are can be an important tool when planning future
      hikes. This hiking level guide will help you plan hikes according to different hike
      ratings set by various websites like All Trails and Modern Hiker. What type of hiker
              are you – novice, moderate, advanced moderate, expert, or expert backpacker?"
          image={image}
          side1="right"
          side2="left"
        />
        <Content
          id="chapter-2" 
          ref={(el) => (chapterRefs.current[1] = el)} 
          textBefore="02"
          textAbove="Hiking Essentials"
          header="Picking the right Hiking Gear!"
          mainText="The nice thing about beginning hiking is that you don’t really need any special gear, you can probably get away with things you already have.
    Let’s start with clothing. A typical mistake hiking beginners make is wearing jeans and regular clothes, which will get heavy and chafe wif they get sweaty or wet."
          image={image2}
          side1="right"
          side2="left"
          reverseContainer={reverseContainer}
          textBeforeStyle={textBeforeStyle}
          textStyle={textStyle}
          pictureStyle={pictureStyle}
        />
        <Content
          id="chapter-3" 
          ref={(el) => (chapterRefs.current[2] = el)} 
          textBefore="03"
          textAbove="where you go is the key"
          header="Understand Your Map & Timing"
          mainText="To start, print out the hiking guide and map. If it’s raining, throw them in a Zip-Lock bag. Read over the guide, study the map, and have a good idea of what to expect. I like to know what my next landmark is as I hike. For example, I’ll read the guide and know that say, in a mile, I make a right turn at the junction.."
          image={image3}
          side1="right"
          side2="left"
        />
        <Footer />
      </div>
    </div>
  );
}

export default App;

步骤2:结合react-spring添加动画效果

如果想要更丝滑的过渡动画,可以用react-spring的useSpring钩子修改SidebarTracker组件:

import { useSpring, animated } from 'react-spring'; 

const SidebarTracker = ({ activeChapter, chapters }) => {
  return (
    <div className="sidebar-tracker" style={{
      position: 'fixed',
      right: '2rem',
      top: '50%',
      transform: 'translateY(-50%)',
      display: 'flex',
      flexDirection: 'column',
      gap: '1.5rem'
    }}>
      {chapters.map((chapter, index) => {
        const props = useSpring({
          color: activeChapter === index ? '#2c3e50' : '#95a5a6',
          fontWeight: activeChapter === index ? 700 : 400,
          borderLeftColor: activeChapter === index ? '#2c3e50' : 'transparent',
          config: { tension: 200, friction: 20 }
        });

        return (
          <animated.button
            key={index}
            onClick={() => {
              document.getElementById(chapter.id).scrollIntoView({ behavior: 'smooth' });
            }}
            style={{
              ...props,
              border: 'none',
              background: 'transparent',
              fontSize: '1rem',
              cursor: 'pointer',
              borderLeft: '3px solid',
              paddingLeft: '0.5rem'
            }}
          >
            {chapter.label}
          </animated.button>
        );
      })}
    </div>
  );
};

步骤3:样式优化

在App.css中添加基础样式,避免侧边栏遮挡内容:

.all-content {
  max-width: 1200px;
  margin: 0 auto;
  padding-right: 8rem; 
}

.tracker-item:hover {
  color: #2c3e50;
}

关键说明

  • 章节判断逻辑:用视口中间位置作为判断点,更符合用户阅读体验;
  • 性能优化:滚动事件中避免频繁DOM操作,只在章节切换时更新状态;
  • react-spring/Parallax适配:如果页面用了Parallax组件,只需把章节的offsetTop计算换成Parallax提供的滚动位置API即可,核心逻辑不变。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 03:33:10