View Transitions API とは
まずは、Google Chromeでこちらをご覧ください。
Chrome Developers で紹介されている View Transitions API が体験できるサイトです。
リンクをクリックするとURLが切り替わっているにも関わらず、シームレスに画面が遷移しています。
このような動作を実現するには、WebサイトをSPA(Single Page Application)で作りつつ、pushState を上手く使うなどの方法がありましたが、 View Transitions API を使えばもっと手軽に実装できるようです。
欠点としては、現在はChrome、Edgeのみの対応(Operaでも一部動作するらしいが未検証)で、Safari、つまりiOSデバイスで動作しないことです。
(上記の デモサイト を見ればわかるのですが、Androidには対応しています)
日本でのiPhoneのシェアを考えると、実践に導入できるのはSafariに対応してからになるとは思うのですが、こういう遷移をさせたいというリクエストがめちゃくちゃくることを見越して、Next.jsの静的書き出しとのコンビネーションを検証しておこうと思います。
今後を見越しての検証なので、App Routeを使用します。静的書き出しで検証しているのは、普段静的書き出しを使うことが多いからです。
Next.js(App Router) + View Transitions API
DEMO
next-view-transitions-api.netlify.app
カードをクリックするとページが遷移するのですが、その際シームレスにアニメーションしてからURLが切り替わります。
検証のためにさささっと実装したので、ブラウザの「戻る」「進む」を使った遷移には未対応です。
リポジトリ
ざっくり解説
Chrome Developer に記載されている通り、document.startViewTransition を使って実装しました。
document.startViewTransitionにはページ遷移時のDOMの動きを指定する関数を渡しておきます。
そして、遷移時にstartViewTransitionが呼び出されると、
- ページの現在の状態をキャプチャ
- startViewTransitionに渡されたコールバック関数を実行(DOMを変更)
- 遷移先のページをキャプチャ
- 遷移先のページに、古いキャプチャと新しいキャプチャを重ねて配置
- 古いキャプチャと新しいキャプチャをクロスフェードで切り替える
という動作をするようです。
詳細は Chrome DeveloperのHow these transitions work を確認してください。
ざっくりとした実装方法
解説の通り、startViewTransitionにDOMの動きを指定する関数を渡します。
document.startViewTransition(() => { // ページ遷移時に実行したいアニメーションの処理を書く });
ただし前述の通り、startViewTransitionはまだ全てのブラウザに対応しているわけではないので、
if (document.startViewTransition) { document.startViewTransition(() => { // ページ遷移時に実行したいアニメーションの処理を書く }); }
という形で、startViewTransitionの存在を確認してから実行する必要があります。
Next.jsを使っている場合、
(document as any).startViewTransition(() => { setIsTransition(true); router.push('/new-page-path'); });
という形で、ステートの変化でDOMさせつつ、新しいページに遷移させるのが簡単な実装になるかと思います。
export function Card({ onClick = function() {}, isTransition = false }: { onClick?: () => void; isTransition?: boolean; }) { return ( <Wrapper className="card" data-is-transition={ String(isTransition) } onClick={ onClick } > <div className="inner"> <img width={ 300 } height={ 169 } src={ '/images/counterattack-of-the-timer.jpg' } /> <div className="body"> <h2>タイトル</h2> <h3>サブタイトル</h3> <p>ディスクリプション</p> </div> </div> </Wrapper> ); }
.card { width: 300px; height: 300px; transition: transform .2s ease-in-out; cursor: pointer; p { opacity: 0; transition: opacity .2s ease-in-out; } &[data-is-select='true'] { height: 400px; transform: scale(1.2); p { opacity: 1; } } }
というような形で、ステートをカスタムデータ属性に渡し、CSSアニメーションでDOMをアニメーションさせるのがお手軽かと思いました。
注意事項
ブラウザの「戻る」「進む」を使って遷移する時は、startViewTransitionが発火しないため別途設定が必要です。
他にももろもろ設定できるのですが、今回は簡単な検証にとどめておきます。
Next.js(App Router) + Nested Layouts
今後を見越した検証も大事なのですが、いますぐSafariにも対応させる方法はないかと考えた結果、getLayoutパターン(App RouterではNested Layouts)を使えば良いのではないかと思い検証してみました。
getLayoutとは
詳しくは こちらの記事 をご覧ください。
ざっくりいうと、ページを遷移しても、アンマウントされないコンポーネントです。
ページのほぼ全てをLayout内に記述すれば、実質SPAのようなものになるのではないかと考えました。
Nested Layoutsとは
詳しくは ドキュメント をご覧ください。
ざっくりいうと、App Routerで使えるgetLayoutのようなものです。
getLayoutはPages Routerでしか使えないため、どうしたものかとドキュメントを検索したときに見つけました。
Migrating the getLayout() pattern to Layouts と紹介されています。
DEMO
next-view-transition.netlify.app
ページ遷移時に、View Transitions API で実装したデモサイトとほぼ同じ動きをします。(厳密にいうとクロスフェードしてません)
ブラウザの「戻る」「進む」にも対応済みです。
しかも、Safariでも問題なく動作するので、iPhoneでも観覧できます。
リポジトリ
ざっくりとした実装方法
解説の通り、Nesting Layoutsを使い、ほぼ全ての部分をRootLayout内に置いています。
src/app/layout.tsx(抜粋)
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="ja"> <body> <AllPageLayout>{ children }</AllPageLayout> </body> </html> ) }
AllPageLayoutという名前が適切なのかはおいておき、サイトのほぼ全ての部分がこちらのコンポーネントに収納されています。
AllPageLayout.tsx(抜粋)
export function AllPageLayout({ children }: { children: ReactNode }) { const [ isSelect, setIsSelect ] = useState(false); const router = useRouter(); function handleClickCard() { router.push('/articles/counterattack-of-the-timer'); } return ( <Wrapper> <main> <Card onClick={ handleClickCard } isSelect={ isSelect } /> </main> <div>{ children }</div> </Wrapper> ); } const Wrapper = styled.div` > main { display: flex; align-items: center; justify-content: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; } `;
なので、実質SPAのような形になり、シームレスなページ遷移に見えるわけです。
静的サイトとして書き出したあとも、 next/router や next/link を使った遷移は内部でいい感じに pushState を実行してくれるので、SPAのように振る舞えます。
実際のところは、ページをリロードした際にアニメーションが見えないように、あれこれ設定しているのですが、今回は触れません。