iT邦幫忙

2025 iThome 鐵人賽

DAY 14
1
Modern Web

在Vibe Coding 時代一起來做沒有AI感的漂亮網站吧!系列 第 14

用 GSAP 在 React 寫跑馬燈效果,讓角色上跑步機吧

  • 分享至 

  • xImage
  •  

嗨咿,我是 illumi!

前端要做「跑馬燈」(marquee) 時,可能會用 CSS animation:

@keyframes marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

雖然簡單,但缺點也很明顯:

  • 控制不靈活(要停下來、加速、跟捲動互動比較麻煩)
  • 跟 React 的生命週期不好整合(unmount 後可能還在跑)

所以一起用 GSAP + React Hook (useGSAP),寫一個可控性更強的跑馬燈吧~
Yes


專案準備

需要安裝:

npm install gsap @gsap/react

然後在 React 裡面可以直接引入:

import { useGSAP } from "@gsap/react";
import gsap from "gsap";

基本結構

先把跑馬燈的結構寫好:

<div ref={marqueeRef} className="w-full overflow-hidden">
  <div ref={trackRef} className="flex gap-4">
    {loopList.map((item, index) => (
      <div key={index} className="shrink-0">
        <TemplateCard {...item} />
      </div>
    ))}
  </div>
</div>

重點:

  • 最外層容器 (marqueeRef):負責「顯示範圍」,超出就隱藏 (overflow-hidden)。
  • 內層軌道 (trackRef):實際會被平移的元素。
  • loopList:我們把清單複製一份 [...list, ...list],這樣才能無縫銜接。

小補充:

在 Figma 做跑馬燈 prototype 也是 套一層外層,再把裡面那層移動,將整體往左和整體往右的兩個元件連上, 用 After 觸發, prototype 中就會自動移動了!


GSAP 動畫邏輯

useGSAP 裡面加上動畫:

useGSAP(
  () => {
    if (!templateList.length || !trackRef.current) return;

    const track = trackRef.current;
    const distance = track.scrollWidth / 2; // 一半距離

    gsap.to(track, {
      x: -distance,        // 向左移動
      duration: 15,        // 跑完整個距離的時間
      repeat: -1,          // 無限循環
      ease: "none",        // 線性移動
      modifiers: {
        // 每次位移超過 distance,就取餘數,形成無縫循環
        x: (x) => `${parseFloat(x) % distance}px`,
      },
      scrollTrigger: {
        trigger: marqueeRef.current,
        start: "top bottom",
        end: "bottom top",
        toggleActions: "play none none pause", // 當畫面滑走就暫停
      },
    });
  },
  { scope: marqueeRef, dependencies: [templateList] }
);

這裡有幾個關鍵點:

  1. distance = scrollWidth / 2

    因為我們複製了一份清單,所以一半距離剛好可以無縫循環。

  2. modifiers

    GSAP 提供 modifiers,可以在每一幀修改動畫數值。

    這裡我們用 % distance,讓它一直 loop,不會突然「跳回去」。

  3. ScrollTrigger

    讓跑馬燈在滑到畫面外時自動暫停,避免浪費效能。


裝飾

最後只要在跑馬燈上面加一個跑動的角色,就有種跑跑步機的感覺了!會比單純跑馬燈更有可愛小巧思!
Yes

完整程式碼

// components/TemplateLoop.tsx
import { useEffect, useRef, useState } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import TemplateCard from "./TemplateCard";

export default function TemplateLoop() {
  const [templateList, setTemplateList] = useState([
    { id: "1", title: "挑戰 A" },
    { id: "2", title: "挑戰 B" },
    { id: "3", title: "挑戰 C" },
  ]);

  const marqueeRef = useRef<HTMLDivElement>(null);
  const trackRef = useRef<HTMLDivElement>(null);

  // GSAP 跑馬燈效果
  useGSAP(
    () => {
      if (!templateList.length || !trackRef.current) return;
      const track = trackRef.current;
      const distance = track.scrollWidth / 2;

      gsap.to(track, {
        x: -distance,
        duration: 15,
        repeat: -1,
        ease: "none",
        modifiers: {
          x: (x) => `${parseFloat(x) % distance}px`,
        },
        scrollTrigger: {
          trigger: marqueeRef.current,
          start: "top bottom",
          end: "bottom top",
          toggleActions: "play none none pause",
        },
      });
    },
    { scope: marqueeRef, dependencies: [templateList] }
  );

  const loopList = [...templateList, ...templateList];

  return (
    <div ref={marqueeRef} className="w-full overflow-hidden">
      <div ref={trackRef} className="flex gap-4">
        {loopList.map((item, index) => (
          <div key={`${item.id}-${index}`} className="shrink-0">
            <TemplateCard title={item.title} />
          </div>
        ))}
      </div>
    </div>
  );
}


進階

  • 加速/減速:用 gsap.to() 即時改變 duration
  • 滑動暫停:用 hover 事件 gsap.killTweensOf(track) 來暫停。

懶得做的寶們可以用之前說的ReactBits 元件:Logo loop

https://ithelp.ithome.com.tw/upload/images/20250915/20178506VIPw9W3oQw.png

好噠~ 今天做到這裡,明天再見啦~

或是大家把跑馬燈發揮成什麼樣子也可以貼在留言給我看!


上一篇
GSAP 最簡單功能 + map 就可以掉落源源不絕的東西~ 觸發掉落特效!
系列文
在Vibe Coding 時代一起來做沒有AI感的漂亮網站吧!14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言