本文へスキップ

SvelteKit 1.24におけるビュー遷移のアンロック

onNavigateによる効率的なページ遷移

ビュー遷移APIは最近、ウェブ開発の世界で大きな注目を集めており、それは当然のことです。このAPIは、2つのページ状態間のアニメーション処理を簡素化し、特にページ遷移に役立ちます。

しかし、これまでSvelteKitアプリではこのAPIを容易に使うことができませんでした。ナビゲーションライフサイクルの適切な場所に組み込むのが難しかったからです。SvelteKit 1.24では、ビュー遷移の統合を大幅に容易にする新しいonNavigateライフサイクルフックが追加されました。詳しく見ていきましょう。

ビュー遷移の仕組み

document.startViewTransitionを呼び出し、DOMを何らかの方法で更新するコールバックを渡すことで、ビュー遷移をトリガーできます。今日の目的では、ユーザーがナビゲーションを行う際にSvelteKitがDOMを更新します。コールバックが完了すると、ブラウザは新しいページ状態に遷移します。デフォルトでは、古い状態と新しい状態間のクロスフェードが行われます。

var document: Documentdocument.startViewTransition(async () => {
	await const domUpdate: () => Promise<void>domUpdate(); // mock function for demonstration purposes
});

ブラウザは、内部で非常に巧妙な処理を行っています。遷移が開始されると、ページの現在の状態をキャプチャし、スクリーンショットを撮ります。そして、DOMが更新されている間、そのスクリーンショットを保持します。DOMの更新が完了すると、新しい状態をキャプチャし、2つの状態間でアニメーションを実行します。

現時点ではChrome(およびその他のChromiumベースのブラウザ)でのみ実装されていますが、WebKitも支持しています。サポートされていないブラウザを使用している場合でも、アニメーションなしのナビゲーションにフォールバックできるため、プログレッシブエンハンスメントに最適です。

ビュー遷移はブラウザAPIであり、SvelteKit APIではないことに注意することが重要です。onNavigateは、今日使用する唯一のSvelteKit固有のAPIです。それ以外は、ウェブで開発を行う場所であればどこでも使用できます!ビュー遷移APIの詳細については、Jake ArchibaldによるChromeの説明記事を強くお勧めします。

onNavigateの仕組み

ビュー遷移の書き方を学ぶ前に、すべてを可能にする関数について説明しましょう:onNavigate

最近まで、SvelteKitには2つのナビゲーションライフサイクル関数がありました。beforeNavigateはナビゲーション開始前に実行され、afterNavigateはナビゲーション後のページ更新後に実行されます。SvelteKit 1.24では、3つ目の関数onNavigateが導入されました。これはすべてのナビゲーションで、新しいページがレンダリングされる直前に実行されます。重要なのは、ページのデータ読み込みが完了した*後*に実行されることです。ビュー遷移を開始するとページとの相互作用が妨げられるため、できるだけ遅く開始する必要があります。

onNavigateからPromiseを返すこともできます。これにより、ビュー遷移が開始されるまでナビゲーションを中断できます。

function function delayNavigation(): Promise<unknown>delayNavigation() {
	return new 
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((res: (value: unknown) => voidres) => function setTimeout(callback: (args: void) => void, ms?: number): NodeJS.Timeout (+2 overloads)setTimeout(res: (value: unknown) => voidres, 100));
} onNavigate(async (navigation) => { // do some work immediately before the navigation completes // optionally return a promise to delay navigation until it resolves return function delayNavigation(): Promise<unknown>delayNavigation(); });

これで準備が整ったので、SvelteKitアプリでビュー遷移を使用する方法を見てみましょう。

ビュー遷移の開始

ビュー遷移を実際に確認する最良の方法は、自分で試してみることです。ローカルターミナルでnpm create svelte@latestを実行するか、StackBlitzのブラウザでSvelteKitデモアプリを起動できます。ビュー遷移APIをサポートするブラウザを使用してください。アプリが実行されたら、src/routes/+layout.svelteのスクリプトブロックに次のコードを追加します。

import { function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<void | (() => void)>): void

A lifecycle function that runs the supplied callback immediately before we navigate to a new URL except during full-page navigations.

If you return a Promise, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use document.startViewTransition. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.

If a function (or a Promise that resolves to a function) is returned from the callback, it will be called once the DOM has updated.

onNavigate must be called during a component initialization. It remains active as long as the component is mounted.

onNavigate
} from '$app/navigation';
function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<void | (() => void)>): void

A lifecycle function that runs the supplied callback immediately before we navigate to a new URL except during full-page navigations.

If you return a Promise, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use document.startViewTransition. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.

If a function (or a Promise that resolves to a function) is returned from the callback, it will be called once the DOM has updated.

onNavigate must be called during a component initialization. It remains active as long as the component is mounted.

onNavigate
((navigation: OnNavigatenavigation) => {
if (!var document: Documentdocument.startViewTransition) return; return new
var Promise: PromiseConstructor
new <void | (() => void)>(executor: (resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => void, reject: (reason?: any) => void) => void) => Promise<void | (() => void)>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => voidresolve) => {
var document: Documentdocument.startViewTransition(async () => { resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => voidresolve(); await navigation: OnNavigatenavigation.Navigation.complete: Promise<void>

A promise that resolves once the navigation is complete, and rejects if the navigation fails or is aborted. In the case of a willUnload navigation, the promise will never resolve

complete
;
}); }); });

これにより、発生するすべてのナビゲーションでビュー遷移がトリガーされます。これは既に動作していることがわかります。デフォルトでは、ブラウザは古いページと新しいページ間でクロスフェードします。

コードの仕組み

このコードは少しわかりにくいかもしれません。興味があれば、1行ずつ説明できますが、今は、これを追加することでナビゲーション中にビュー遷移APIとやり取りできるようになることを知っていれば十分です。

前述のように、onNavigateコールバックは、ナビゲーション後の新しいページがレンダリングされる直前に実行されます。コールバック内では、document.startViewTransitionが存在するかどうかを確認します。存在しない場合(つまり、ブラウザがサポートしていない場合)、早期に終了します。

次に、ビュー遷移が開始されるまでナビゲーションの完了を遅らせるためにPromiseを返します。ビュー遷移が開始されるまでPromiseが解決されるタイミングを制御できるように、Promiseコンストラクタを使用します。

return new 
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((resolve: (value: unknown) => voidresolve) => {
var document: Documentdocument.startViewTransition(async () => { resolve: (value: unknown) => voidresolve(); await navigation.complete; }); });

Promiseコンストラクタ内では、ビュー遷移を開始します。ビュー遷移コールバック内では、返されたPromiseを解決します。これは、SvelteKitにナビゲーションを完了するよう指示します。ビュー遷移を開始した*後*にナビゲーションが完了するのを待つことが重要です。ブラウザは古い状態のスナップショットを撮る必要があるため、新しい状態に遷移できます。

最後に、ビュー遷移コールバック内で、navigation.completeを待機することで、SvelteKitがナビゲーションを完了するのを待ちます。navigation.completeが解決されると、新しいページがDOMに読み込まれ、ブラウザは2つの状態間でアニメーションを実行できます。

少し複雑ですが、抽象化しないことで、ビュー遷移を直接操作し、必要なカスタマイズを行うことができます。

CSSによる遷移のカスタマイズ

CSSアニメーションを使用して、このページ遷移をカスタマイズすることもできます。+layout.svelteのスタイルブロックに次のCSSルールを追加します。

@keyframes fade-in {
	from {
		opacity: 0;
	}
}

@keyframes fade-out {
	to {
		opacity: 0;
	}
}

@keyframes slide-from-right {
	from {
		transform: translateX(30px);
	}
}

@keyframes slide-to-left {
	to {
		transform: translateX(-30px);
	}
}

:root::view-transition-old(root) {
	animation:
		90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
		300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

:root::view-transition-new(root) {
	animation:
		210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
		300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

これで、ページ間を移動すると、古いページはフェードアウトして左にスライドし、新しいページはフェードインして右からスライドします。これらの特定のアニメーションスタイルは、Jake Archibaldの優れたChrome Developersの記事「ビュー遷移」からのもので、このAPIでできることをすべて理解したい場合は、読む価値があります。

::view-transition擬似要素の前に:rootを追加する必要があることに注意してください。これらの要素はドキュメントのルートのみに存在するため、Svelteがコンポーネントにスコープを適用しないようにする必要があります。

ヘッダーが古いページと新しいページの両方で同じであるにもかかわらず、ページ全体がスライドインおよびスライドアウトすることに気付いたかもしれません。よりスムーズな遷移を実現するために、ヘッダーに一意のview-transition-nameを指定して、ページの残りの部分とは別にアニメーションするようにすることができます。src/routes/Header.svelteで、スタイルブロックのheader CSSセレクターを探し、ビュー遷移名を追加します。

header {
	display: flex;
	justify-content: space-between;
	view-transition-name: header;
}

これで、ヘッダーはナビゲーション時に遷移しなくなりますが、ページの残りの部分は遷移します。

型の修正

startViewTransitionはすべてのブラウザでサポートされているわけではないため、IDEはそれが存在することを認識していない可能性があります。エラーを解消し、正しい型を取得するには、app.d.tsに次のコードを追加します。

declare global {
	// preserve any customizations you have here
	namespace App {
		// interface Error {}
		// interface Locals {}
		// interface PageData {}
		// interface Platform {}
	}

	// add these lines
	interface ViewTransition {
		updateCallbackDone: Promise<void>;
		ready: Promise<void>;
		finished: Promise<void>;
		skipTransition: () => void;
	}

	interface Document {
		startViewTransition(updateCallback: () => Promise<void>): ViewTransition;
	}
}

export {};

個々の要素の遷移

要素にview-transition-nameを指定すると、ページの残りのアニメーションから分離されることを確認しました。view-transition-nameを設定すると、ブラウザは遷移完了後にその新しい位置にスムーズにアニメーションするように指示されます。view-transition-nameは一意の識別子として機能するため、ブラウザは古い状態と新しい状態の一致する要素を識別できます。

それがどのようなものかを見てみましょう。デモアプリのナビゲーションには、アクティブなページを示す小さな三角形があります。現在、ナビゲーション後、新しい位置に突然表示されます。ブラウザが代わりに新しい位置にアニメーションするように、view-transition-nameを指定しましょう。

src/routes/Header.svelteで、アクティブなページインジケーターを作成するCSSルールを見つけ、view-transition-nameを指定します。

li[aria-current='page']::before {
	/* other existing rules */
	view-transition-name: active-page;
}

その1行を追加するだけで、インジケーターはジャンプするのではなく、スムーズに新しい位置にスライドするようになります。

(違いに気付かないかもしれません。画面上部の小さな移動する三角形のインジケーターを見てください!)

モーションの削減

ウェブでアニメーションを実装する際には、ユーザーのモーション設定を尊重することが重要です。極端なページ遷移を実装できるからといって、実装する必要があるという意味ではありません。モーションを削減することを好むユーザーのためにすべてのページ遷移を無効にするには、グローバルなstyles.cssに次のコードを追加できます。

@media (prefers-reduced-motion) {
	::view-transition-group(*),
	::view-transition-old(*),
	::view-transition-new(*) {
		animation: none !important;
	}
}

これは最も安全なオプションかもしれませんが、モーションの削減は必ずしもアニメーションの無効化を意味するわけではありません。代わりに、ビュー遷移をケースバイケースで検討できます。たとえば、スライドアニメーションを無効にする代わりに、デフォルトのクロスフェード(モーションを含まない)を残すことができます。これを行うには、無効にしたい::view-transitionルールをprefers-reduced-motion: no-preferenceメディアクエリでラップします。

@media (prefers-reduced-motion: no-preference) {
	:root::view-transition-old(root) {
		animation:
			90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
			300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
	}

	:root::view-transition-new(root) {
		animation:
			210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
			300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
	}
}

次のステップ

ご覧のとおり、SvelteKitはビュー遷移の*仕組み*について多くのことを抽象化していません。NuxtやAstroにあるようなフレームワークの抽象化ではなく、ブラウザの組み込みdocument.startViewTransition::view-transition APIを直接操作しています。私たちは、人々がSvelteKitアプリでビュー遷移をどのように使用し、将来独自のより高レベルの抽象化を追加する必要があるかどうかを知りたがっています。

リソース

この投稿のデモコードはGitHubで見つけることができ、ライブバージョンはVercelにデプロイされています。以下は、役立つ可能性のあるその他のビュー遷移のリソースです。