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

如何为React实现的无限轮播组件添加过渡动画效果

React + TypeScript 无限轮播顺滑过渡实现方案

核心实现逻辑

采用预克隆首尾节点的方案替代滑动时动态增删节点,初始化阶段就把原列表首项克隆到列表末尾、末项克隆到列表开头,总渲染节点数 = 原节点数 + 2,避免频繁DOM操作打断动画流程。

过渡开关逻辑

  • 正常用户滑动/自动轮播时,始终开启过渡效果,给滑动轨道添加transition: transform 0.3s ease-in-out即可实现顺滑位移
  • 仅当滑动到首尾克隆节点时,才需要临时禁用过渡跳转位置:
    • 向右滑动到末尾克隆的首节点时,立刻禁用过渡,将轨道位置跳转到原列表首项对应位置
    • 向左滑动到开头克隆的末节点时,立刻禁用过渡,将轨道位置跳转到原列表末项对应位置
      跳转操作因为没有过渡,用户完全无感知,不会出现闪屏或跳变感。

onTransitionEnd 事件正确使用

要避免事件冒泡和重复触发问题:

  1. 绑定事件时仅处理滑动轨道自身触发的过渡结束事件,忽略子元素的过渡事件
  2. 组件卸载时及时移除事件监听,避免内存泄漏
  3. 跳转位置的逻辑全部放在该事件回调中执行,确保上一段滑动动画完全结束后再执行位置重置

代码示例

import { useState, useRef, useCallback, useEffect, useMemo } from 'react';

// 轮播项数据类型
interface CarouselItem {
  id: string | number;
  content: React.ReactNode;
}

interface InfiniteCarouselProps {
  items: CarouselItem[];
}

export const InfiniteCarousel = ({ items }: InfiniteCarouselProps) => {
  const trackRef = useRef<HTMLDivElement>(null);
  // 初始索引从1开始,跳过开头的克隆末项
  const [currentIndex, setCurrentIndex] = useState(1);
  const [enableTransition, setEnableTransition] = useState(true);
  const originalLength = items.length;

  // 预添加首尾克隆节点的渲染列表
  const renderItems = useMemo(() => {
    if (originalLength === 0) return [];
    return [items[originalLength - 1], ...items, items[0]];
  }, [items, originalLength]);

  const handleTransitionEnd = useCallback((e: TransitionEvent) => {
    // 仅处理轨道自身的过渡结束事件
    if (e.target !== trackRef.current) return;

    // 滑到末尾克隆首项,跳回原列表首项
    if (currentIndex === originalLength + 1) {
      setEnableTransition(false);
      requestAnimationFrame(() => setCurrentIndex(1));
    }
    // 滑到开头克隆末项,跳回原列表末项
    if (currentIndex === 0) {
      setEnableTransition(false);
      requestAnimationFrame(() => setCurrentIndex(originalLength));
    }
  }, [currentIndex, originalLength]);

  useEffect(() => {
    const track = trackRef.current;
    if (!track) return;
    track.addEventListener('transitionend', handleTransitionEnd);
    return () => track.removeEventListener('transitionend', handleTransitionEnd);
  }, [handleTransitionEnd]);

  // 滑动触发逻辑
  const handleNext = () => {
    setEnableTransition(true);
    setCurrentIndex(prev => prev + 1);
  };

  const handlePrev = () => {
    setEnableTransition(true);
    setCurrentIndex(prev => prev - 1);
  };

  // 轨道样式
  const trackStyle: React.CSSProperties = useMemo(() => ({
    transform: `translateX(-${currentIndex * 100}%)`,
    transition: enableTransition ? 'transform 0.3s ease-in-out' : 'none',
    display: 'flex',
    width: `${renderItems.length * 100}%`,
  }), [currentIndex, enableTransition, renderItems.length]);

  if (originalLength === 0) return null;

  return (
    <div style={{ overflow: 'hidden', width: '100%' }}>
      <div ref={trackRef} style={trackStyle}>
        {renderItems.map((item, idx) => (
          <div key={`${item.id}-${idx}`} style={{ width: '100%' }}>
            {item.content}
          </div>
        ))}
      </div>
      <button onClick={handlePrev}>上一张</button>
      <button onClick={handleNext}>下一张</button>
    </div>
  );
};

注意事项

  • 每个轮播项的宽度要统一,且占满父容器宽度,避免位移计算出错
  • 如果需要支持触摸滑动,在touchstart/touchend事件中处理逻辑时,也要遵守先开过渡、过渡结束后再判断是否需要跳转的规则
  • 自动轮播逻辑要在用户交互时暂停,避免多次滑动冲突导致索引异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 01:06:04