ThisWeb Logo
This.Web
所有文章React 效能優化實戰課
  1. 首頁
  2. 所有文章
  3. 前端
  4. 如何在 Next.js 專案加上 Page Transition?- 提高網頁質感的好方法

如何在 Next.js 專案加上 Page Transition?- 提高網頁質感的好方法

前端

ThisWeb

資深前端工程師

發佈/更新於

2026年5月25日

免費訂閱電子報!

和 2000+ 工程師一起學習軟體、AI 開發技巧,每週一收穫 1 篇技術內容、1 段職涯分享、1 個最新資訊!

免費訂閱電子報!

和 2000+ 工程師一起學習軟體、AI 開發技巧,每週一收穫 1 篇技術內容、1 段職涯分享、1 個最新資訊!

Page Transition(頁面轉場) 一直是很流行的網頁技術和效果,他指的是使用者從一個頁面切換到另一個頁面時,中間所發生的視覺變化。

為什麼需要頁面轉場 Page Transition?

當使用者從一個頁面前往另一個頁面時,如果畫面突然硬切、出現短暫白屏、載入過慢或讓使用者失去原本的操作脈絡,都可能讓人感到中斷和困惑,甚至提高離開的機率。

例如我用我自己的網站當作範例,原生網站在切換頁面時,網頁會直接跳轉,造成使用者體驗上的割裂感。

而頁面轉場 Page Transition 的目的就是要降低這種斷裂感,只要頁面之間的過渡做得好,就可以保留使用者的注意力、提供視覺的連續性和正面回饋來增強使用者的體驗,同時也能讓網頁更美觀和有趣來加強品牌形象。

如何使用 Page Transition

由於這個專案是使用 Next.js 開發,所以也會使用 Next.js 當作範例,一步一步帶你做出這種 Page Transition 的效果。

使用 Page Transition 有 2 種方法:

  1. 使用 View Transition API
  2. 使用 JavaScript 做頁面切換

View Transition API 是瀏覽器支援的,因此能做到更多種的效果,例如這個網站,就是把前後頁面的內容同時顯示在網頁上,並用推移的方式把舊的內容推走

但 View Transition API 的缺點是支援度還沒有很好,可以看到目前 FireFox 只有部分功能支援,一些比較舊的電腦也可能不知的這個功能

所以目前我的專案,是使用第 2 種方法製作 Page Transition,也就是用 JavaScript 製作,這樣就沒有支援度上的問題了!

Note

如果你想使用 View Transition API,可以參考 next-view-transitions 套件。

1. 安裝 next-transition-router

如果 Next.js 專案要使用 Page Transition,最快速的方式就是使用這個 Library:next-transition-router:

Note

如果你使用原生 JavaScript,可以參考這篇文章:Different Page Transitions For Different Circumstances

bash
pnpm add next-transition-router

安裝後,我們新增一個 <TransitionRouterProvider/> 來使用套件提供的 <TransitionRouter />,他可以傳 auto prop 來自動監聽所有的 Next 連結:

tsx
"use client";

import { useRef } from "react";
import { animate } from "motion";
import { TransitionRouter } from "next-transition-router";

export function TransitionRouterProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const wrapperRef = useRef<HTMLDivElement>(null!);

  return (
    <TransitionRouter
      auto
      leave={(next) => {
        animate(
          wrapperRef.current,
          { opacity: [1, 0] },
          { duration: 0.5, onComplete: next },
        );
      }}
      enter={(next) => {
        animate(
          wrapperRef.current,
          { opacity: [0, 1] },
          { duration: 0.5, onComplete: next },
        );
      }}
    >
      <div ref={wrapperRef}>{children}</div>
    </TransitionRouter>
  );
}

並且在 layout.tsx 中使用,基本就完成了:

tsx
<body>
  <TransitionRouterProvider>{children}</TransitionRouterProvider>
</body>

2. 增加更多 Page Transition 效果

如果你只需要淡入淡出的效果,這樣就已經做到了,但如果你想要再增添更多動畫,來增強網站的品牌感,可以參考我目前網站效果的思路:

  1. 離開時:頁面內容 opacity 1 → 0,並搭配 translateY 向上位移;深色 overlay 從畫面底部向上展開,覆蓋目前頁面。
  2. 進入時:路由切換後,overlay 移出畫面顯示新內容;新頁面內容 opacity 0 → 1,並由 translateY 位移狀態回到初始位置。

要實現這樣的 Page Transition,我們需要多一個元素來做遮罩,並且在 wrapperRef 增加 translate 的效果,

我們先新增一個元素遮罩,使用 clip-path:polygon(0%_100%,100%_100%,100%_100%,0%_100%) 來讓遮罩的初始樣式是隱藏的:

tsx
<div
  ref={overlayRef}
  className="fixed inset-0 top-0.75 z-router-overlay h-full w-full bg-primary [clip-path:polygon(0%_100%,100%_100%,100%_100%,0%_100%)]"
/>

因為要控制這個遮罩,所以也要使用一個 overlayRef 來抓取 DOM 元素

tsx
const overlayRef = useRef<HTMLDivElement>(null!);

接著處理 leave 和 enter 的邏輯:

tsx
<TransitionRouter
  auto
  leave={(next) => {
    animate([
      [
        overlayRef.current,
        {
          clipPath: leaveClipPath,
        },
        {
          duration: duration,
          ease: VIEW_TRANSITION_EASING,
          at: 0,
        },
      ],
      [
        wrapperRef.current,
        {
          opacity: [1, 0],
          y: leaveY,
        },
        {
          y: {
            duration: duration,
            ease: VIEW_TRANSITION_EASING,
          },
          opacity: {
            duration: duration,
            ease: "linear",
          },
          at: 0,
        },
      ],
    ]).then(next);
  }}
  enter={(next) => {
    animate([
      [
        overlayRef.current,
        {
          clipPath: enterClipPath,
        },
        {
          duration: duration,
          ease: VIEW_TRANSITION_EASING,
          at: 0,
        },
      ],
      [
        wrapperRef.current,
        {
          opacity: [0, 1],
          y: enterY,
        },
        {
          y: {
            duration: duration,
            ease: VIEW_TRANSITION_EASING,
          },
          opacity: {
            duration: duration,
            ease: "linear",
          },
          at: 0,
        },
      ],
    ]).then(next);
  }}
>
  <div
    ref={overlayRef}
    className="fixed inset-0 top-0.75 z-router-overlay h-full w-full bg-primary [clip-path:polygon(0%_100%,100%_100%,100%_100%,0%_100%)]"
  />
  <div ref={wrapperRef}>{children}</div>
</TransitionRouter>

最後,我使用 useReducedMotion 在希望不要太多動畫的裝置上,減少 Page Transition 的效果,例如取消 Overlay 和位移,以下是完整的程式碼:

tsx
const VIEW_TRANSITION_EASING = [0.9, 0, 0.1, 1] as const;

export function TransitionRouterProvider({
  children,
  footerData,
}: {
  children: React.ReactNode;
  footerData: FooterData;
}) {
  const overlayRef = useRef<HTMLDivElement>(null!);
  const wrapperRef = useRef<HTMLDivElement>(null!);

  const shouldReduceMotion = useReducedMotion();
  const leaveClipPath = shouldReduceMotion
    ? null
    : [
        "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
        "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
      ];
  const enterClipPath = shouldReduceMotion
    ? null
    : [
        "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
        "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
      ];
  const leaveY = shouldReduceMotion ? 0 : [0, -240];
  const enterY = shouldReduceMotion ? 0 : [240, 0];

  const duration = shouldReduceMotion ? 0.4 : 0.8;

  return (
    <TransitionRouter
      auto
      leave={(next) => {
        animate([
          [
            overlayRef.current,
            {
              clipPath: leaveClipPath,
            },
            {
              duration: duration,
              ease: VIEW_TRANSITION_EASING,
              at: 0,
            },
          ],
          [
            wrapperRef.current,
            {
              opacity: [1, 0],
              y: leaveY,
            },
            {
              y: {
                duration: duration,
                ease: VIEW_TRANSITION_EASING,
              },
              opacity: {
                duration: duration,
                ease: "linear",
              },
              at: 0,
            },
          ],
        ]).then(next);
      }}
      enter={(next) => {
        animate([
          [
            overlayRef.current,
            {
              clipPath: enterClipPath,
            },
            {
              duration: duration,
              ease: VIEW_TRANSITION_EASING,
              at: 0,
            },
          ],
          [
            wrapperRef.current,
            {
              opacity: [0, 1],
              y: enterY,
            },
            {
              y: {
                duration: duration,
                ease: VIEW_TRANSITION_EASING,
              },
              opacity: {
                duration: duration,
                ease: "linear",
              },
              at: 0,
            },
          ],
        ]).then(next);
      }}
    >
      <div
        ref={overlayRef}
        className="fixed inset-0 top-0.75 z-router-overlay h-full w-full bg-primary [clip-path:polygon(0%_100%,100%_100%,100%_100%,0%_100%)]"
      />

      <div ref={wrapperRef}>{children}</div>
    </TransitionRouter>
  );
}

使用 Page Transition 要注意的事情

雖然 Page Transition 可以讓網站的切換更有記憶點,也能讓新舊頁面之間的關係更清楚。

不過,它本質上仍然是一段的動畫,如果時間太長、效果太多,或每次切換都過度強調,反而會讓使用者覺得網站變慢,甚至造成操作上的干擾。

以下是幾個在使用 Page Transition 時需要注意的重點。

1. 時間不要太長

大多數 UI 動畫的時間建議落在 100ms 到 500ms 之間,實際長度會取決於動畫的複雜度與元素移動距離。動畫太快,使用者可能看不清楚狀態變化;但動畫太慢,則會讓人覺得被迫等待。

Page Transition 屬於比較大範圍的畫面切換,因此可以比一般 micro interaction 稍微長一些,不過我會建議整體體感時間控制在 500ms 到 1600ms 左右;如果超過 1600ms,使用者就會開始感覺到延遲。

2. 動畫要有方向感

動畫的目的除了裝飾以外,重點應該是要幫助使用者理解狀態、路徑與內容關係,而不只是讓畫面看起來比較炫。所以 page transition 要注意:

  1. 從哪裡離開
  2. 從哪裡進入
  3. 新舊頁面之間的層級關係和方向關係

這也是我在網站中讓舊頁面往上離開、新頁面由下往上進入的原因。這樣的動線會讓切換更像是往下一個內容段落前進的感覺,而不是單純把兩個頁面硬接在一起。

3. 需要注意效能

Page Transition 通常會影響整個畫面,所以效能比一般小型互動更重要。

建議在動畫上,優先考慮 opacity、transform 等效果,如果要加上 blur 、clippath、 box-shadow 或 width、 height 等 layout 變化,就需要謹慎一點,因為可能讓在切換頁面時,出現卡頓的效果。

4. 支援 reduced motion

prefers-reduced-motion 可以偵測使用者是否在系統層級要求減少動畫。對於不想要動畫的使用者,應該減少或避免不必要的動畫。

我的做法是減少動畫效果,而沒有完全取消。

next-transition-router 的實現邏輯

到這邊,我們已經完成了 Page Transition 了,但我很好奇這個 Library 是如何實現這樣的效果的,所以我去看了他的原始程式碼,結果發現實現的方式也不難,我們自己手寫一個也不需要花太多時間。

這個套件核心只有 8 KB 左右,本質上是透過**控制反轉(Inversion of Control)**與 React Hook 生命週期清理機制,來達到以下流程:

  1. 離場動畫
  2. 路由更新
  3. 入場動畫

第一步:攔截導航行為

要實現轉場動畫,第一步要先攔截所有觸發頁面跳轉的行為,阻止瀏覽器與 Next.js 預設立刻換頁的動作。

這個庫提供了自定義的 <Link> 元件來手動攔截。但如果專案規模很大,我們不想一個一個去替換原生的 Next.js Link,最方便的做法是直接使用 Provider 的 auto={true} 功能。

auto 模式:全域事件代理

因為 Next.js 的 <Link> 最終在 DOM 中都是帶有 href 的 <a> 標籤,

所以開啟 auto 後,Provider 會用 delegate-it 在 document 全域監聽所有 a[href] 的 click 事件,符合條件就用 event.preventDefault() 阻止原生換頁並接管導航,不需要手動替換每個 Link 的 import。

tsx
useEffect(() => {
  if (!auto) return;
  const controller = new AbortController();
  delegate("a[href]", "click", handleClick, { signal: controller.signal });
  return () => controller.abort();
}, [auto, handleClick]);

為什麼要用 delegate-it?

delegate-it 解決的是**事件委派(event delegation)**的問題。

直接自己監聽的話,你會這樣寫:

javascript
document.querySelectorAll('a[href]').forEach((link) => {
  link.addEventListener('click', handleClick);
});

這有兩個問題:

  1. 抓不到之後才渲染的連結:Next.js 換頁後,新頁面的 <a> 是之後才掛進 DOM 的,之前的 querySelectorAll 不會包含它們,所以新連結點了沒有轉場效果。
  2. 要自己處理 cleanup:每個 listener 都要記得移除,N 個連結就要移除 N 次。

delegate-it 的做法是把單一個 listener 掛在 document 上,利用事件冒泡攔截所有 a[href] 的點擊:

javascript
delegate('a[href]', 'click', handleClick, { signal: controller.signal });

不管連結是什麼時候渲染的,點擊事件都會冒泡到 document 被攔截到。cleanup 也只需要 controller.abort() 一次搞定,非常簡單直接。

安全過濾:shouldLinkTriggerTransition

但我們不能盲目攔截所有連結。

如果使用者按住 Command 或 Ctrl 鍵(想在新分頁開啟)、連結標記為 target="_blank" 或 download,或是連到外部網站,都應該讓瀏覽器執行原生動作。

所以這個套件會使用 shouldLinkTriggerTransition 先做判斷

typescript
export function shouldLinkTriggerTransition(
  link: HTMLAnchorElement,
  event: any,
): boolean {
  return (
    link.target !== "_blank" &&
    link.origin === window.location.origin &&
    link.rel !== "external" &&
    !link.download &&
    !isModifiedEvent(event) &&
    !event.defaultPrevented
  );
}

useTransitionRouter:重構程式化導航

除了點擊連結外,使用者也可能用 router.push('/about') 來切換路由。

所以如果要使用這種方式,必須使用套件提供的 useTransitionRouter ,他會重寫原生的 push 與 replace,統一路由的行為。

tsx
export function useTransitionRouter() {
  const router = useRouter();
  const pathname = usePathname();
  const { navigate } = useTransitionState();

  const push = useCallback(
    (href: string, options?: NavigateOptions) => {
      navigate(href, pathname, "push", options);
    },
    [pathname, navigate],
  );
  // ...
}

第二步:狀態機與動畫生命週期核心

攔截完導航後,需要一個全域狀態機來協調「離場動畫 → 路由跳轉 → 入場動畫」的順序。

這個套件設計了三個狀態:"none"(靜止)、"leaving"(離場中)、"entering"(入場中)。

navigate:延遲路由跳轉,呼叫離場動畫

當 navigate 被呼叫時,並不會直接換頁,而是把真正的路由切換包成 next() 回調,傳給開發者定義的 leave。

typescript
const next = () => router[method](href, options);
// ...
setStage("leaving");
leaveRef.current = await leave(next, pathname, href);

頁面狀態設為 "leaving" 後,離場動畫開始播放;動畫結束時,開發者手動呼叫 next(),Next.js 才真正換頁。

React effect cleanup:自動偵測路由完成

這是整個套件最精妙的設計在於 pathname 改變(代表 Next.js 換頁完成)時,React 在執行新 effect 前,會先執行上一次 effect 的 cleanup。

這個 cleanup 讀到的舊 stage 是 "leaving",就在這個「新頁面 DOM 掛載、舊 effect 卸載」的瞬間,自動將狀態切換到 "entering"。

typescript
useEffect(() => {
  return () => {
    if (stage === "leaving") {
      setStage("entering");
    }
  };
}, [stage, pathname]);

入場動畫與狀態歸零

stage 切換到 "entering" 時,會觸發一個 effect 執行開發者傳入的 enter 入場動畫。

傳給 enter 的參數是一個把 stage 重設為 "none" 的回調,當動畫結束呼叫它,狀態完整歸零。

typescript
useEffect(() => {
  if (stage === "entering") {
    const runEnter = async () => {
      enterRef.current = await Promise.resolve(enter(() => setStage("none")));
    };
    runEnter();
  }
}, [stage, enter]);

總結

Page Transition 做得好,對於使用者來說是大大的加分,而實現方式其實也不會太過困難,使用 next-transition-router 就能做到。

next-transition-router 的本身其實不複雜,核心就是兩件事:

  1. 攔截導航,把 router.push 包成 next() 傳給使用者,讓動畫決定換頁時機
  2. 靠 useEffect cleanup 偵測換頁完成,在 pathname 改變的瞬間自動切換狀態,銜接入場動畫

完整流程圖

用流程圖來表示就是:

  1. 使用 delegate-it 監聽 Link 點擊事件(如果使用 auto={true})
  2. 使用者點擊連結或呼叫 router.push() 觸發 navigate() 函式
  3. 符合轉場條件?(同源、非 hash、非同頁)
  4. setStage("leaving"),呼叫 leave(next, from, to) 函式
  5. 播放離場動畫,動畫結束,開發者呼叫 next()
  6. router.push() 執行,Next.js 開始換頁
  7. pathname 改變
  8. useEffect([stage, pathname]) cleanup 執行,舊 stage === "leaving" → setStage("entering")
  9. useEffect([stage, enter]) 觸發,呼叫 enter(() => setStage("none"))
  10. 播放入場動畫
  11. 動畫結束,開發者呼叫回調 next() 執行 setStage("none")

整個流程不依賴任何動畫庫,只借助 React 本身的生命週期就可以做到離場 → 換頁 → 入的順序,非常值得學習~!

文章參考

  • https://github.com/ismamz/next-transition-router
  • https://github.com/shuding/next-view-transitions
  • https://frontendmasters.com/blog/different-page-transitions-for-different-circumstances/

下一篇看什麼?

01.

10 大設計網站提升你的審美!讓你的網站更有質感

02.

2026 前端框架怎麼選?React、Vue、Angular 完整比較指南

03.

React 是什麼?2026 完整新手學習指南

文章目錄

  1. 為什麼需要頁面轉場 Page Transition?
  2. 如何使用 Page Transition
  3. 1. 安裝 next-transition-router
  4. 2. 增加更多 Page Transition 效果
  5. 使用 Page Transition 要注意的事情
  6. 1. 時間不要太長
  7. 2. 動畫要有方向感
  8. 3. 需要注意效能
  9. 4. 支援 reduced motion
  10. next-transition-router 的實現邏輯
  11. 第一步:攔截導航行為
  12. 第二步:狀態機與動畫生命週期核心
  13. 總結
  14. 完整流程圖

訂閱電子報!

和 2000+ 人一起學習 AI、軟體與網站實作資訊。

或來信合作:kun@thisweb.dev

頁面導覽

  • 首頁
  • 所有文章

聯絡資訊

THISWEB