轉場的本質

頁面轉場要解決的問題只有一個:在舊頁消失與新頁出現之間,填補那段空窗。空窗處理得差,使用者看到白屏閃爍;處理得好,使用者感受到的是連續的空間。

舊頁 → 離開動畫 → 數據載入 → 內容替換 → 進場動畫 → 新頁

onNavigate:官方掛載點

SvelteKit 提供 onNavigate,回傳的 Promise 會讓框架等待你再完成導航:

import { onNavigate } from '$app/navigation';

onNavigate((navigation) => {
	if (!document.startViewTransition) return;

	return new Promise((resolve) => {
		document.startViewTransition(async () => {
			resolve();
			await navigation.complete;
		});
	});
});

這是 View Transitions API 的用法。但如果你想完全控制動畫 — 遮罩、時序、緩動 — GSAP 是更好的選擇

onNavigate(async ({ from, to }) => {
	if (!from || !to || from.url.pathname === to.url.pathname) return;

	await pageLeave(); // GSAP timeline:舊頁淡出 + 遮罩進場
});

常見錯誤

用 beforeNavigate + cancel + goto

// ❌ 有狀態卡死風險
beforeNavigate(({ cancel, to }) => {
	if (isTransitioning) return;
	cancel();
	animateOut().then(() => goto(to.url));
});

goto 失敗或被中斷時,isTransitioning 永遠不會被重置,導航徹底卡死。

動畫沒有超時兜底

動畫因為任何原因沒有 resolve,導航就永遠阻塞。一律加上:

const MAX_LEAVE = 1.2;
gsap.delayedCall(MAX_LEAVE, () => tl.progress(1));

遮罩轉場

最經典的 Awwwards 風格轉場是一塊全屏遮罩(curtain):

gsap
	.timeline()
	.to('.overlay', { scaleY: 1, transformOrigin: 'bottom', duration: 0.5, ease: 'power4.inOut' })
	.to('.overlay', { scaleY: 0, transformOrigin: 'top', duration: 0.5, ease: 'power4.out' });

遮罩蓋上時替換內容,掀開時新頁已就緒 — 使用者永遠看不到空窗。

小結

  • onNavigate,不要用 cancel + goto
  • 所有轉場動畫都要有超時兜底
  • prefers-reduced-motion 時直接跳過動畫
  • 轉場前後記得清理與重建 ScrollTrigger