概要
AstroのView Transitions機能を使って、MPAで非同期的な遷移ができないか試してみました。
結論、astro:before-swapイベント内で使用できる関数swap()を使うことによって、自由度の高い遷移アニメーションを実現できそうです。
実装方針として、astro:before-swap内では、元ページのドキュメントdocumentと、遷移先ページのドキュメントe.newDocumentが取得できるので、それを手動で差し替えていく形になります。
新旧のドキュメントを同時に参照できるので、元ページと遷移先ページが共存する遷移アニメーションを作成できます。(参考:MIMEYOI)
実装例
例えば、ページ遷移時に画面全体を覆うカバーを挟む場合、以下のような構造にして、カバーが覆ったタイミングでmain部分を差し替えます。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<!-- ★忘れずに -->
<ClientRouter />
</head>
<body>
<!-- HeaderとTransitionCoverコンポーネントは差し替えない -->
<Header />
<TransitionCover />
<!-- mainのみ差し替える -->
<main>
<slot />
</main>
</body>
</html>
document.addEventListener('astro:before-swap', (e) => {
// スクロール位置を保存する
sessionStorage.setItem('scrollPosition', window.scrollY.toString())
e.swap = () => {
const tl = gsap.timeline()
tl.add(TransitionCover.Element!.animeIn()) // カバーイン
tl.call(() => {
// mainを差し替える
const newMain = e.newDocument.querySelector('main')
const oldMain = document.querySelector('main')
if (newMain && oldMain) {
oldMain.replaceWith(newMain)
// ページタイトルの差し換え
document.title = e.newDocument.title
} else {
document.body.replaceWith(e.newDocument.body)
}
})
tl.add(TransitionCover.Element!.animeOut()) // カバーアウト
}
}
document.addEventListener('astro:after-swap', (e) => {
// スクロール位置を戻す
const scrollPosition = sessionStorage.getItem('scrollPosition')
if (scrollPosition) {
window.scrollTo({ top: Number(scrollPosition), behavior: 'instant' })
sessionStorage.removeItem('scrollPosition')
}
})
swapに関するドキュメント(翻訳)
以下、Astro Building a custom swap functionのChatGPT翻訳になります。
カスタム swap 関数の構築
背景
- Astro の View Transitions は、ナビゲーション時にページ間でスムーズなアニメーションを提供します。 (docs.astro.build)
- デフォルトでは、Astro は組み込みの
swap実装を使って古いページの DOM を新しいページの DOM に置き換えます。ですが、必要に応じてこの置き換えロジックをカスタマイズできます。 (docs.astro.build) - そのために、
astro:transitions/clientモジュールが提供するユーティリティ関数群を活用します。 (docs.astro.build)
利用できるユーティリティ関数 (swapFunctions)
swapFunctions オブジェクトは以下のような関数を提供し、これらを組み合わせてカスタムな swap 実装を構築できます。 (docs.astro.build)
| 関数名 | 概要 |
|---|---|
deselectScripts(newDocument: Document) |
新しいドキュメント中新たに読み込まれた <script> タグのうち、再実行不要なものにマークをつける(すでに現在のドキュメントにあるスクリプトなど) (docs.astro.build) |
swapRootAttributes(newDocument: Document) |
<html> 要素(ドキュメントのルート)の属性を、新しいドキュメントの属性で置き換える。たとえば lang 属性や、内部で付与される data-astro-transition のような属性も対象。これを呼び出さないと、アニメーションや transition の方向指定が正しく動作しなくなる可能性がある。 (docs.astro.build) |
swapHeadElements(newDocument: Document) |
<head> 内の要素を差分で更新。現在のドキュメントの <head> にあるうち、新しいページに存在しないものを削除し、新しいページの <head> にある要素を追加する。スタイルやスクリプトなど head 内リソースを適切に更新するために重要。 (docs.astro.build) |
saveFocus() → returns () => void
|
現在フォーカスされている要素を記憶し、swap 後にフォーカスを復元するための関数を返す。ユーザー入力状態やフォーカスの保持に役立つ。 (docs.astro.build) |
swapBodyElement(newBody: Element, oldBody: Element) |
古いページの <body> を、新しいページの <body> に置き換える。ただし、transition:persist 指定された要素など、永続させたい要素があれば、それらを新しい DOM にマッチさせて維持する。 (docs.astro.build) |
カスタム swap の基本テンプレート
たとえば、Astro のデフォルトの swap 動作を再現するカスタム実装は、以下のように書くことができます。 (docs.astro.build)
import { swapFunctions } from "astro:transitions/client";
function mySwap(doc: Document) {
swapFunctions.deselectScripts(doc);
swapFunctions.swapRootAttributes(doc);
swapFunctions.swapHeadElements(doc);
const restoreFocusFunction = swapFunctions.saveFocus();
swapFunctions.swapBodyElement(doc.body, document.body);
restoreFocusFunction();
}
document.addEventListener("astro:before-swap", (event) => {
event.swap = () => mySwap(event.newDocument);
});
上記のように、astro:before-swap イベントで event.swap に自前の関数を割り当てることで、デフォルト置き換えロジックをカスタマイズできます。 (docs.astro.build)
このテンプレートを土台にして、例えば次のようなカスタム処理を挟むことも可能です:
- head 要素の差分更新後に特定のスクリプトの実行制御を追加
- body の置き換え前後で特定の DOM の変換/クリーンアップ
- フォーカス復元の前に任意の UI 更新(スクロール、スタイル、状態保存… など)
注意点と活用のコツ
-
swapRootAttributesを呼び出すことは非常に重要です。これを怠ると、transition の方向や内部的な属性 (data-astro-transitionなど) が正しく反映されず、アニメーションやスタイルが壊れる可能性があります。 (docs.astro.build) -
saveFocus→restoreFocusFunctionを使うことで、ページ遷移前にフォーカスがあった要素があれば、遷移後も同じ要素にフォーカスを戻せます。アクセシビリティやキーボード操作の観点から便利です。 -
transition:persistを使って「永続させたい要素」を指定している場合は、swapBodyElementによってそれら要素の移行が適切に処理されます。 - 必要に応じて、上記テンプレートの各ステップ間に「独自処理(カスタムロジック)」を挟むことで、非常に柔軟なページ遷移体験を構築できます。