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

基于Framer Motion实现React元素平滑无缝循环滚动

实现无缝垂直滚动循环的解决方案

针对你用Tailwind CSS + React + Framer Motion做50个元素垂直滚动时的无缝衔接问题,核心矛盾在于原生repeat: Infinity会在动画结束后重置元素位置,导致生硬跳变。下面给出两种可行的实现方案:

方案一:克隆元素 + 容器位移动画(适合大量元素,性能更优)

核心思路是给原列表克隆一份接在后面,让容器做垂直位移动画,当动画执行到原列表滚完时,瞬间重置容器位置,此时克隆的列表刚好衔接上,用户完全感知不到断层。

import { motion, useAnimation } from "framer-motion";
import { useEffect, useRef } from "react";

const InfiniteVerticalScroll = () => {
  const controls = useAnimation();
  const containerRef = useRef(null);
  // 单个元素固定高度(这里用h-16,对应64px)
  const itemHeight = 64;
  const totalItems = 50;
  // 生成原元素列表
  const baseItems = Array.from({ length: totalItems }).map((_, i) => i);
  // 克隆一份接在后面,实现视觉无缝
  const extendedItems = [...baseItems, ...baseItems];
  // 动画总位移:刚好滚完所有原元素的高度
  const animationTotal = totalItems * itemHeight;

  useEffect(() => {
    const runScroll = async () => {
      await controls.start({
        y: -animationTotal,
        transition: {
          duration: 20, // 滚动总时长,按需调整
          ease: "linear", // 线性动画保证速度均匀
          repeat: Infinity,
          // 动画重复时瞬间重置位置,克隆元素刚好衔接
          onRepeat: () => controls.set({ y: 0 }),
        },
      });
    };
    runScroll();
  }, [controls, animationTotal]);

  // 交错排列样式:奇数索引元素加左侧偏移
  const getOffsetClass = (index) => index % 2 === 1 ? "ml-4" : "";

  return (
    <div className="h-96 overflow-hidden relative">
      <motion.div
        ref={containerRef}
        animate={controls}
        className="absolute top-0 left-0"
      >
        {extendedItems.map((itemIndex, renderIndex) => (
          <motion.div
            key={`scroll-item-${renderIndex}`}
            className={`h-16 w-3/4 bg-blue-500 mb-2 rounded-lg flex items-center px-4 text-white ${getOffsetClass(itemIndex)}`}
            // 元素初始渐入动画,增强平滑感
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.3, delay: itemIndex * 0.02 }}
          >
            Item {itemIndex + 1}
          </motion.div>
        ))}
      </motion.div>
    </div>
  );
};

export default InfiniteVerticalScroll;

关键细节

  • 克隆元素:extendedItems把原列表复制一份,确保原元素滚出视口时,克隆元素刚好接上,视觉无断层
  • 瞬间重置:onRepeat回调里用controls.set({ y: 0 })把容器拉回初始位置,此时克隆的第一个元素和原列表最后一个元素完全连续,用户看不到重置动作
  • 固定高度:必须保证所有元素高度一致,否则动画位移计算会出错;如果元素高度不固定,可以用useLayoutEffect动态计算总高度
  • 交错效果:通过getOffsetClass给奇数索引元素加左侧偏移,配合逐个延迟的初始动画,实现平滑的交错入场

方案二:AnimatePresence + 元素位置轮换(适合少量元素,逻辑更简洁)

如果不想克隆元素,可以用AnimatePresence管理元素的进入/退出动画,当元素滚出视口顶部时,把它移到列表底部重新入场。

import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";

const InfiniteVerticalScrollAlt = () => {
  const [items, setItems] = useState(Array.from({ length: 50 }).map((_, i) => i));
  const itemHeight = 64;

  useEffect(() => {
    // 定时轮换元素位置:把第一个元素移到末尾
    const interval = setInterval(() => {
      setItems(prev => [...prev.slice(1), prev[0]]);
    }, 300); // 每个元素滚动间隔,按需调整

    return () => clearInterval(interval);
  }, []);

  const getOffsetClass = (index) => index % 2 === 1 ? "ml-4" : "";

  return (
    <div className="h-96 overflow-hidden relative">
      <div className="absolute top-0 left-0">
        <AnimatePresence mode="popLayout">
          {items.map((itemIndex) => (
            <motion.div
              key={itemIndex}
              className={`h-16 w-3/4 bg-blue-500 mb-2 rounded-lg flex items-center px-4 text-white ${getOffsetClass(itemIndex)}`}
              initial={{ y: itemHeight, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: -itemHeight, opacity: 0 }}
              transition={{ duration: 0.5, ease: "easeInOut" }}
            >
              Item {itemIndex + 1}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </div>
  );
};

export default InfiniteVerticalScrollAlt;

注意事项

  • 这个方案通过数组操作轮换元素位置,适合元素数量较少的场景;如果是50个元素,频繁操作数组可能会有轻微性能损耗,优先选方案一
  • AnimatePresence的mode="popLayout"确保元素退出后不占用布局空间,避免列表高度跳动

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 19:46:35