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

ReactJS等速无限滚动动画:多元素显示问题解决方案咨询

保持滚动动画速度恒定的实现方案

当前代码的核心问题是CSS动画时长固定,当元素数量增加时,容器总高度变大,相同时间内滚动的距离变长,导致速度变快。要保持速度不变,必须让动画时长和滚动距离成正比——滚动距离越长,动画时长越久。


方案一:动态计算CSS动画时长

思路

原来5个元素时,滚动完所有元素(一遍)用了5秒,相当于每秒滚动1个元素的高度。我们可以基于这个速度,根据当前元素总数动态计算动画时长:

  1. 获取单个元素的总高度(包含margin)
  2. 计算滚动总距离(即一遍元素的总高度,因为容器里复制了一遍元素,动画滚动-50%刚好覆盖)
  3. 时长 = 滚动总距离 / 单元素高度(比如100个元素用100秒,保持每秒滚动1个元素的速度)

修改后的代码

const { useRef, useEffect, useState } = React;

function Card({ cardName }) {
  return (
    <div className="bubble">
      <div className="card m-2 pt-2">
        <div className="py-1">
          <div className="fs-5 mt-2">{cardName}</div>
        </div>
      </div>
    </div>
  );
}

// 模拟100个元素的数据
let data = Array.from({length: 100}, (_, i) => ({Name: `${i+1}`}));

function App() {
  const ref = useRef(null);
  const [containerWidth, setWidth] = useState("100%");
  const [animationState, setPlay] = useState("paused");
  const [animationDuration, setAnimationDuration] = useState("5s");

  useEffect(() => {
    if (ref.current) {
      const scrollHeight = ref.current.scrollHeight;
      setWidth(scrollHeight + "px");

      // 获取单个card的总高度(含margin)
      const firstCard = ref.current.querySelector(".card");
      if (firstCard) {
        const cardStyle = getComputedStyle(firstCard);
        const cardTotalHeight = 
          firstCard.offsetHeight + 
          parseInt(cardStyle.marginTop) + 
          parseInt(cardStyle.marginBottom);
        // 滚动总距离是一遍元素的高度,即scrollHeight/2
        const totalScrollDistance = scrollHeight / 2;
        // 按每秒滚动1个元素的速度计算时长
        const duration = (totalScrollDistance / cardTotalHeight) + "s";
        setAnimationDuration(duration);
      }

      setPlay("running");
    }
  }, [data]); // 元素数据变化时重新计算

  const renderCards = data.map((el, index) => {
    return <Card key={index} cardName={el.Name} />;
  });

  return (
    <div className="App">
      <div
        className="d-flex"
        ref={ref}
        style={{
          width: containerWidth,
          animationPlayState: animationState,
          animationDuration: animationDuration
        }}
      >
        {renderCards}
        {renderCards}
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
.App {
  overflow: hidden;
}
.card {
  width: 200px !important;
  height: 200px;
  background: #ffefef;
  box-shadow: 0px 1px 4px 1px rgba(158, 151, 151, 0.25);
  border-radius: 15px;
  margin: 12px;
  padding: 12px;
}
.d-flex {
  overflow: visible;
  animation: animateContainer linear forwards infinite;
  flex-direction: column;
}
@keyframes animateContainer {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-50%);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div id="app"></div>

方案二:用requestAnimationFrame实现精准速度控制

如果觉得CSS动画的动态时长不够灵活,可以用JS的requestAnimationFrame直接控制滚动速度,完全脱离CSS动画的时长限制。

思路

  1. 定义固定的滚动速度(比如224px/秒,对应原来单个元素的总高度)
  2. 实时记录滚动位置,每一帧根据时间差更新位置
  3. 当滚动位置超过一遍元素的总高度时,重置位置,实现无缝循环

代码实现

const { useRef, useEffect, useState } = React;

function Card({ cardName }) {
  return (
    <div className="bubble">
      <div className="card m-2 pt-2">
        <div className="py-1">
          <div className="fs-5 mt-2">{cardName}</div>
        </div>
      </div>
    </div>
  );
}

let data = Array.from({length: 100}, (_, i) => ({Name: `${i+1}`}));

function App() {
  const containerRef = useRef(null);
  const [containerHeight, setContainerHeight] = useState("auto");
  const scrollRef = useRef({
    position: 0,
    lastTime: Date.now(),
    speed: 224, // 每秒滚动224px(对应原来单个元素的总高度)
    animationId: null
  });

  useEffect(() => {
    if (!containerRef.current) return;

    const firstCard = containerRef.current.querySelector(".card");
    if (!firstCard) return;

    // 获取单个card总高度,计算一遍元素的总高度
    const cardStyle = getComputedStyle(firstCard);
    const cardTotalHeight = 
      firstCard.offsetHeight + 
      parseInt(cardStyle.marginTop) + 
      parseInt(cardStyle.marginBottom);
    const singleLoopHeight = cardTotalHeight * data.length;

    // 设置容器高度为两倍(用于滚动循环)
    setContainerHeight((singleLoopHeight * 2) + "px");

    // 启动滚动动画
    const animate = () => {
      const now = Date.now();
      const deltaTime = (now - scrollRef.current.lastTime) / 1000; // 转换为秒
      scrollRef.current.position += scrollRef.current.speed * deltaTime;
      scrollRef.current.lastTime = now;

      // 当滚动超过一遍元素高度时,重置位置,实现无缝循环
      if (scrollRef.current.position >= singleLoopHeight) {
        scrollRef.current.position -= singleLoopHeight;
      }

      containerRef.current.style.transform = `translateY(-${scrollRef.current.position}px)`;
      scrollRef.current.animationId = requestAnimationFrame(animate);
    };

    scrollRef.current.animationId = requestAnimationFrame(animate);

    // 组件卸载时停止动画
    return () => {
      cancelAnimationFrame(scrollRef.current.animationId);
    };
  }, [data]);

  const renderCards = data.map((el, index) => {
    return <Card key={index} cardName={el.Name} />;
  });

  return (
    <div className="App">
      <div
        className="d-flex"
        ref={containerRef}
        style={{ height: containerHeight }}
      >
        {renderCards}
        {renderCards}
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
.App {
  overflow: hidden;
}
.card {
  width: 200px !important;
  height: 200px;
  background: #ffefef;
  box-shadow: 0px 1px 4px 1px rgba(158, 151, 151, 0.25);
  border-radius: 15px;
  margin: 12px;
  padding: 12px;
}
.d-flex {
  overflow: visible;
  display: flex;
  flex-direction: column;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div id="app"></div>

两种方案对比

  • CSS方案:更简洁,依赖CSS动画,适合简单场景,动态计算时长即可。
  • JS方案:更灵活,能精确控制速度,还可随时暂停/调整速度,适合复杂交互场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 00:39:56