如何用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; `;
关键注意事项
- 放弃
mirror模式:反向滚动会导致视觉断层,改用loop模式配合双列表拼接,实现平滑循环。 - 滚动距离精准匹配:动画滚动距离严格等于单份列表的宽度,重置位置时第二份列表刚好衔接第一份的开头。
- 适配大屏场景:若大屏仍有空白,可拼接三份列表(
[...awardsData, ...awardsData, ...awardsData]),确保动画重置前新内容已覆盖屏幕。 - 线性动画:使用
ease: "linear"保证滚动速度均匀,强化无缝感。
内容的提问来源于stack exchange,提问作者Alex Smagin
相关产品推荐
相关产品推荐

