View Transitions APIでアニメーション付きの画面遷移を行う
View Transitions APIを使用した画面遷移の実装の覚書です。
View Transitions APIとは
View Transitions APIは、ページ間の移行やUI要素の状態変更をスムーズにアニメーション化するためのWeb APIです。
このAPIを使用することで、Webページの視覚的な変化をスムーズにできます。
シングルページアプリケーション(SPA)やマルチページアプリケーション(MPA)の両方で利用できます。
以下に、View Transitions APIの基本的な使用方法を説明します。
今回作るもの
以下のように、メイン画面とログイン画面をスムーズに遷移するようなシングルページアプリケーションを作成します。
JavaScriptフレームワークはReactを使用し、スタイリングはTailwind.cssを使用しています。
また、ルーティングにはReact Router V6を使用しています。
以下からは実装手順を記載します。
画面を作る
今回はサンプルのため、適当な画面を作成します。
React Routerでホーム画面とログイン画面で表示するコンポーネントが切り替わるようにします。
import { FiHome, FiUser, FiArrowLeft } from "react-icons/fi";
import { Routes, Route, useNavigate } from "react-router-dom";
function App() {
const navigate = useNavigate();
return (
<main className="select-none h-screen max-h-screen flex flex-col bg-gradient-to-b from-slate-50 to-slate-200">
{/* ヘッダー */}
<header className="p-4 w-full">
<Routes>
<Route path="/" element={
<div className="flex items-center ml-0 mr-auto font-bold text-xl text-slate-950">
<FiHome className="w-5 h-5 mr-4" />ホーム
</div>
}/>
<Route path="/login" element={
<div className="flex items-center ml-0 mr-auto font-bold text-xl text-slate-950">
<FiArrowLeft onClick={() => { navigate(-1) }} className="w-5 h-5 mr-4" />ログイン
</div>
}/>
</Routes>
</header>
{/* メインコンテンツ */}
<section className="flex-grow overflow-y-auto">
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/login" element={ <Login /> } />
</Routes>
</section>
{/* フッター */}
<footer className="bg-white flex">
<Routes>
<Route path="/" element={
<div className="mx-auto my-2">
<button onClick={() => { navigate('/login') }} className="flex items-center px-2.5 py-1 rounded-full text-blue-800 border border-blue-800 hover:text-white hover:bg-blue-800 transition-all">
<FiUser className="mr-2"/>ログイン
</button>
</div> }/>
<Route path="/login" element={ <></> }/>
</Routes>
</footer>
</main>
);
}
View Transitions APIを呼び出す
通常の画面遷移が実装できたら、画面遷移を行う際にdocument.startViewTransition
を呼び出すだけで、デフォルトのアニメーション(クロスフェード)を付けてくれます。
今回は、React Routerのnavigate
で画面遷移を行っていますので、トランジション付きの遷移をおこなうhookとしてラップします。
注意点として、View Transitions APIは新しいAPIのため、まだ対応していないブラウザもあります。
startViewTransition
がUndefinedの場合、通常のトランジションなしの画面遷移を行うようにします。
また、Reactは画面を非同期に更新します。
startViewTransition
に渡す画面更新処理はflushSync
でラップして、同期的に更新したほうが良いかもしれません。
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { flushSync } from 'react-dom';
export const useNavigationTransition = () => {
const navigate = useNavigate();
const navigationTransition = useCallback(
async (newRoute) => {
if (!document.startViewTransition) {
// Transition APIに非対応のブラウザの場合は通常の遷移
return navigate(newRoute);
}
document.startViewTransition({
update: () => {
// 画面更新処理
flushSync(() => {
navigate(newRoute);
});
}
});
return;
},
[navigate]
);
return {
navigationTransition
};
};
import { FiHome, FiUser, FiArrowLeft } from "react-icons/fi";
-import { Routes, Route, useNavigate } from "react-router-dom";
+import { Routes, Route } from "react-router-dom";
+import { useNavigationTransition } from "./navigationTransition";
function App() {
- const navigate = useNavigate();
+ const { navigationTransition } = useNavigationTransition();
return (
<main className="select-none h-screen max-h-screen flex flex-col bg-gradient-to-b from-slate-50 to-slate-200">
...
<Route path="/login" element={
<div className="flex items-center ml-0 mr-auto font-bold text-xl text-slate-950">
- <FiArrowLeft onClick={() => { navigate(-1) }} className="w-5 h-5 mr-4" />ログイン
+ <FiArrowLeft onClick={() => { navigationTransition(-1) }} className="w-5 h-5 mr-4" />ログイン
</div>
}/>
</Routes>
...
<Routes>
<Route path="/" element={
<div className="mx-auto my-2">
- <button onClick={() => { navigate('/login') }} className="flex items-center px-2.5 py-1 rounded-full text-blue-800 border border-blue-800 hover:text-white hover:bg-blue-800 transition-all">
+ <button onClick={() => { navigationTransition('/login') }} className="flex items-center px-2.5 py-1 rounded-full text-blue-800 border border-blue-800 hover:text-white hover:bg-blue-800 transition-all"> <FiUser className="mr-2"/>ログイン
</button>
</div> }/>
これだけで、以下のようなクロスフェードで画面が切り替わるアプリを実装できます。
アニメーションをカスタマイズする
ここからは、スライドで画面が切り替わるようにアニメーションをカスタマイズします。
ブラウザはstartViewTransition
が呼び出されると、画面の更新を一時停止し、アンマウントされる要素とマウントされる要素のキャプチャをとります。
以下のような疑似要素ツリーが作成され、::view-transition-old
と::view-transition-new
に要素のキャプチャがそれぞれ割り当てられます。
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
なので、::view-transition-old
と::view-transition-new
にCSSでアニメーションを設定することで、アニメーションをカスタマイズできます。
また、startViewTransition
に渡すオプションでアニメーションのタイプを自由に設定できます。
これによって、ページの進む、戻るなどのアクションに応じて個別にアニメーションを設定することができます。
今回はナビゲーションの際に文字列でパスを渡された場合はforwards
、-1
が渡された場合はbackwards
として、それぞれにアニメーションを設定します。
document.startViewTransition({
update: () => {
// 画面更新処理
flushSync(() => {
navigate(newRoute);
});
},
+ types: [newRoute < 0 ? 'backwards' : 'forwards']
});
html:active-view-transition-type(forwards) {
&::view-transition-old(root) {
animation: 300ms ease-in both slide-to-left,
100ms ease 200ms both fade-out;
}
&::view-transition-new(root) {
animation: 300ms ease-in both slide-from-right,
200ms ease 100ms both fade-in;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(root) {
animation: 300ms ease-in reverse both slide-from-right,
100ms ease 200ms both fade-out;
}
&::view-transition-new(root) {
animation: 300ms ease-in reverse both slide-to-left,
200ms ease 100ms both fade-in;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes slide-from-right {
from {
transform: translateX(50%);
}
}
@keyframes slide-to-left {
to {
transform: translateX(-50%);
}
}
以上で以下のようにページの進む、戻るで画面全体がスライドするようになったかと思います。
最後に、要素ごとに個別のアニメーションを設定します。
今回はヘッダーはアニメーションせず、メインコンテンツのみスライドするようにします。
CSSのview-transition-name
で要素ごとに名前を自由に設定できます。
ヘッダーとメインコンテンツにTailwind.cssで以下のように名前を設定します。
...
{/* ヘッダー */}
<header className="[view-transition-name:header] p-4 w-full">
<Routes>
<Route path="/" element={
<div className="flex items-center ml-0 mr-auto font-bold text-xl text-slate-950">
<FiHome className="w-5 h-5 mr-4" />ホーム
</div>
}/>
...
{/* メインコンテンツ */}
<section className="[view-transition-name:main-contents] flex-grow overflow-y-auto">
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/login" element={ <Login /> } />
</Routes>
</section>
CSSを以下のように変更します。
注意点として、startViewTransition
ではデフォルトでアニメーションが設定されています。
なので、アニメーションしたくない場合は、animation: none
でアニメーションを切る必要があります。
html:active-view-transition-type(forwards) {
&::view-transition-old(main-contents) {
animation: 300ms ease-in both slide-to-left,
100ms ease 200ms both fade-out;
}
&::view-transition-new(main-contents) {
animation: 300ms ease-in both slide-from-right,
200ms ease 100ms both fade-in;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(main-contents) {
animation: 300ms ease-in reverse both slide-from-right,
100ms ease 200ms both fade-out;
}
&::view-transition-new(main-contents) {
animation: 300ms ease-in reverse both slide-to-left,
200ms ease 100ms both fade-in;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes slide-from-right {
from {
transform: translateX(50%);
}
}
@keyframes slide-to-left {
to {
transform: translateX(-50%);
}
}
html:active-view-transition-type(forwards, backwards) {
&::view-transition-old(header) {
opacity: 1;
animation: none;
}
&::view-transition-new(header) {
opacity: 0;
animation: none;
}
}
以上で、最初の例のような画面遷移が実装できます。