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

如何用React与Framer Motion实现无缝无限滚动轮播?

实现React + Framer Motion无缝水平无限滚动轮播

问题场景

使用React和Framer Motion制作图标水平无限滚动轮播时,动画可正常运行,但循环末尾会出现可见空白区域,在电视、超宽显示器等大屏设备上尤为明显。即便重复多份数据源,或通过动态计算数据份数适配屏幕宽度,仍无法消除滚动末尾的空隙。

当前组件代码:

import { motion } from "framer-motion";
import { AwardsContainer, AwardsImg } from "./Awards.styled";

const textAnimate = {
  initial: { x: 0 },
  animate: {
    x: "-55%",
    transition: {
      repeat: Infinity,
      repeatType: "mirror",
      duration: 25,
    },
  },
};

function Awards() {
  const repeatedAwards = [...awardsData, ...awardsData];

  return (
    <AwardsContainer>
      <motion.div variants={textAnimate} initial="initial" animate="animate">
        {repeatedAwards.map(({ id, src, alt }, index) => (
          <AwardsImg key={`${id}-${index}`} src={src} alt={alt} />
        ))}
      </motion.div>
    </AwardsContainer>
  );
}

export default Awards;

解决方案

核心思路:通过两份完全相同的列表拼接,配合精准匹配单份列表宽度的滚动动画,替代mirror反向滚动模式,实现视觉上的无缝循环。

优化后的完整代码

import { useRef, useEffect, useState } from "react";
import { motion } from "framer-motion";
import { AwardsContainer, AwardsImg, AwardsList } from "./Awards.styled";

function Awards() {
  const [listWidth, setListWidth] = useState(0);
  const listRef = useRef(null);

  // 获取单份列表的实际宽度
  useEffect(() => {
    if (listRef.current) {
      setListWidth(listRef.current.offsetWidth);
    }
  }, []);

  // 监听窗口 resize,实时更新列表宽度
  useEffect(() => {
    const handleResize = () => {
      if (listRef.current) {
        setListWidth(listRef.current.offsetWidth);
      }
    };
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  // 滚动动画配置:滚动单份列表宽度后立即重置,无限循环
  const scrollAnimation = {
    animate: {
      x: [0, -listWidth],
      transition: {
        x: {
          repeat: Infinity,
          duration: 25,
          ease: "linear",
          repeatType: "loop",
          repeatDelay: 0,
        },
      },
    },
  };

  return (
    <AwardsContainer>
      <div style={{ overflow: "hidden", width: "100%" }}>
        <motion.div
          variants={scrollAnimation}
          animate="animate"
          style={{ display: "flex" }}
        >
          {/* 第一份列表,用于测量宽度 */}
          <AwardsList ref={listRef}>
            {awardsData.map(({ id, src, alt }) => (
              <AwardsImg key={id} src={src} alt={alt} />
            ))}
          </AwardsList>
          {/* 第二份列表,实现无缝衔接 */}
          <AwardsList>
            {awardsData.map(({ id, src, alt }) => (
              <AwardsImg key={`clone-${id}`} src={src} alt={alt} />
            ))}
          </AwardsList>
        </motion.div>
      </div>
    </AwardsContainer>
  );
}

export default Awards;

配套样式(Awards.styled.js)

确保列表横向排列且不收缩,保证宽度测量准确:

import styled from "styled-components";

export const AwardsContainer = styled.div`
  width: 100%;
  overflow: hidden;
`;

export const AwardsList = styled.div`
  display: flex;
  gap: 2rem; // 图标间距,可按需调整
  flex-shrink: 0; // 禁止列表收缩,确保宽度稳定
`;

export const AwardsImg = styled.img`
  height: 60px;
  width: auto;
`;

关键注意事项

  1. 放弃mirror模式:反向滚动会导致视觉断层,改用loop模式配合双列表拼接,实现平滑循环。
  2. 滚动距离精准匹配:动画滚动距离严格等于单份列表的宽度,重置位置时第二份列表刚好衔接第一份的开头。
  3. 适配大屏场景:若大屏仍有空白,可拼接三份列表([...awardsData, ...awardsData, ...awardsData]),确保动画重置前新内容已覆盖屏幕。
  4. 线性动画:使用ease: "linear"保证滚动速度均匀,强化无缝感。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:43:23