SvelteKitでView Transitions APIを使う方法

# はじめに
SvelteKit [v1.24.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%401.24.0)がリリースされ、[onNavigate](https://kit.svelte.dev/docs/modules#$app-navigation-onnavigate)というライフサイクル関数が追加されました。
[onNavigate](https://kit.svelte.dev/docs/modules#$app-navigation-onnavigate)を使用することでURLに移動する直前に指定したコールバックを実行することができます。
[onNavigate](https://kit.svelte.dev/docs/modules#$app-navigation-onnavigate)を使うことでSvelteKitで[View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)を使用して、リッチなページ遷移を実現する方法を説明します。

# 注意
View Transitions APIは`Chrome 111` 以上、または `Edge 111` 以上 で使用できます。
FirefoxやSafariでは今のところ動かないので注意してください。

# SvelteKitプロジェクトのセットアップ

まず、SvelteKitプロジェクトをセットアップします。以下のコマンドで新しいプロジェクトを作成できます。
テンプレートを選択できますが、`SvelteKit demo app`を選択してください。

```bash
npm create svelte@latest my-app

┌  Welcome to SvelteKit!
│
◆  Which Svelte app template?
│  ● SvelteKit demo app (A demo app showcasing some of the features of SvelteKit - play a word guessing game that works without JavaScript!)
│  ○ Skeleton project
│  ○ Library project
└
```


次に、プロジェクトディレクトリに移動します。

```bash
cd my-app
```

依存関係をインストールします。

```bash
npm install
```

# View Transitions APIを使って、リッチなページ遷移を実装する

## `+layout.svelte`を編集する

```diff_html:src/routes/+layout.svelte
<script>
	import Header from './Header.svelte';
	import './styles.css';
+   import { onNavigate } from '$app/navigation';

+   onNavigate(() => {
+    if (!document.startViewTransition) return;
+
+    return new Promise((resolve) => {
+      document.startViewTransition(async () => {
+        resolve();
+        await navigation.complete;
+      });
+	 });
+	});
</script>

<div class="app">
	<Header />

	<main>
		<slot />
	</main>

	<footer>
		<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
	</footer>
</div>

<style>
	.app {
		display: flex;
		flex-direction: column;
		min-height: 100vh;
	}

	main {
		flex: 1;
		display: flex;
		flex-direction: column;
		padding: 1rem;
		width: 100%;
		max-width: 64rem;
		margin: 0 auto;
		box-sizing: border-box;
	}

	footer {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		padding: 12px;
	}

	footer a {
		font-weight: bold;
	}

	@media (min-width: 480px) {
		footer {
			padding: 12px 0;
		}
	}
</style>
```

## `Header.svelte`を編集する

```diff_html:src/routes/Header.svelte
<script>
  import { page } from "$app/stores";
  import logo from "$lib/images/svelte-logo.svg";
  import github from "$lib/images/github.svg";
</script>

<header>
  <div class="corner">
    <a href="https://kit.svelte.dev">
      <img src={logo} alt="SvelteKit" />
    </a>
  </div>

  <nav>
    <svg viewBox="0 0 2 3" aria-hidden="true">
      <path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
    </svg>
    <ul>
      <li aria-current={$page.url.pathname === "/" ? "page" : undefined}>
        <a href="/">Home</a>
      </li>
      <li aria-current={$page.url.pathname === "/about" ? "page" : undefined}>
        <a href="/about">About</a>
      </li>
      <li
        aria-current={$page.url.pathname.startsWith("/sverdle")
          ? "page"
          : undefined}
      >
        <a href="/sverdle">Sverdle</a>
      </li>
    </ul>
    <svg viewBox="0 0 2 3" aria-hidden="true">
      <path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
    </svg>
  </nav>

  <div class="corner">
    <a href="https://github.com/sveltejs/kit">
      <img src={github} alt="GitHub" />
    </a>
  </div>
</header>

<style>
  header {
    display: flex;
    justify-content: space-between;
  }

  .corner {
    width: 3em;
    height: 3em;
  }

  .corner a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
  }

  .corner img {
    width: 2em;
    height: 2em;
    object-fit: contain;
  }

  nav {
    display: flex;
    justify-content: center;
    --background: rgba(255, 255, 255, 0.7);
  }

  svg {
    width: 2em;
    height: 3em;
    display: block;
  }

  path {
    fill: var(--background);
  }

  ul {
    position: relative;
    padding: 0;
    margin: 0;
    height: 3em;
    display: flex;
    justify-content: center;
    align-items: center;
    list-style: none;
    background: var(--background);
    background-size: contain;
  }

  li {
    position: relative;
    height: 100%;
  }

  li[aria-current="page"]::before {
    --size: 6px;
    content: "";
    width: 0;
    height: 0;
    position: absolute;
    top: 0;
    left: calc(50% - var(--size));
    border: var(--size) solid transparent;
    border-top: var(--size) solid var(--color-theme-1);
  }

  nav a {
    display: flex;
    height: 100%;
    align-items: center;
    padding: 0 0.5rem;
    color: var(--color-text);
    font-weight: 700;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    text-decoration: none;
    transition: color 0.2s linear;
  }

  a:hover {
    color: var(--color-theme-1);
  }

+  li[aria-current="page"]::before {
+    view-transition-name: active-page;
+  }
</style>
```

## `app.d.ts`を編集する
`startViewTransition`がIDE等でうまく認識してくれなくて型エラーになる場合があります。

```diff_typescript:src/app.d.ts
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
	namespace App {
		// interface Error {}
		// interface Locals {}
		// interface PageData {}
		// interface Platform {}
	}

+	interface ViewTransition {
+        updateCallbackDone: Promise<void>;
+        ready: Promise<void>;
+        finished: Promise<void>;
+        skipTransition: () => void;
+    }

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

export {};

```

## キャプチャ画面
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/f35fc760-3968-665a-b6f0-66dbb2a6449a.gif)


オレンジのインジケーターがスムーズに動くようになりました :tada: :tada: :tada: 

# さいごに

今回は、SvelteKit v1.24.0で追加されたonNavigateを用いて、View Transitions APIを使ったリッチなページ遷移の実装方法を説明しました。

この実装により、ユーザのナビゲーション体験を向上させることが可能だと思うので是非試してみてください！