view-transitions について
DOM の状態変更やページ遷移にアニメーションを加える Web 開発向けスキルです。View Transitions API、要素単位の CSS 制御、別方式のナビゲーションアニメーションを説明します。
主な機能
- 01document.startViewTransition の利用
- 02遷移要素への名前の指定
- 03ページ遷移状態のアニメーション
- 04データ読み込みと遷移のタイミング調整
- 05代替となる退出・登場アニメーション
活用シーン
- 01詳細パネルを開く際のアニメーション
- 02写真一覧と写真詳細の間の遷移
- 03Web アプリへのナビゲーションアニメーション追加
- 04読み込みが遅いページ変更の方式選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill view-transitionsインストール後、Claude Code で/view-transitionsを入力して読み込まれたか確認します。
詳細
ビュートランジションのアニメーション
目次
使用するタイミング
- ページの状態やナビゲーションの切り替えをアニメーション化したい場合に使用します
- Web アプリケーションで、洗練されたアプリのようなナビゲーション体験を実現するのに役立ちます
手順
- DOM の変更をアニメーション化するには、
document.startViewTransition(callback)を使用します - 状態間で切り替える要素には、一意の
view-transition-nameCSS プロパティを割り当てます - API を使用する前に、ブラウザーが対応しているか確認します(
if (document.startViewTransition)) - データの取得が完了してからトランジションを開始し、DOM が固定される時間を最小限に抑えます
- View Transitions API にまだ対応していないブラウザー向けに、CSS アニメーションによるフォールバックを検討します
詳細
View Transitions の概要
View Transitions API は、視覚的な DOM の変更を任意の状態から次の状態へ簡単に遷移させる方法を提供します。コンテンツの表示・非表示の切り替えといった小さな変更から、あるページから次のページへの移動といった大きな変更まで対応できます。
JavaScript API の中心となるのは document.startViewTransition(callback) です。ここでの callback は、通常、DOM を新しい状態に更新する関数です。
簡単な例として、<details> 要素の表示・非表示を切り替えてみましょう。
if (document.startViewTransition) {
// (check for browser support)
document.addEventListener("click", function (event) {
if (event.target.matches("summary")) {
event.preventDefault(); // (we'll toggle the element ourselves)
const details = event.target.closest("details");
document.startViewTransition(() => details.toggleAttribute("open"));
}
});
}
document.startViewTransition は、コールバックを呼び出す前に現在の DOM のスクリーンショットを撮影します。ここでは、コールバックで open 属性を切り替えるだけです。完了すると、ブラウザーは最初のスクリーンショットと新しい表示の間を遷移できます。
これらの古い表示と新しい表示は疑似要素として表示され、それぞれ CSS の ::view-transition-old(root) と ::view-transition-new(root) で参照できます。たとえば、遷移を目立たせるため、次のように animation-duration を長くできます。
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 2s;
}
View Transitions では、デフォルトのクロスフェードにとどまらない、より高度なアニメーションで複数の変更をアニメーション化することもできます。特定の要素に CSS の view-transition-name と layout または paint の containment を設定すると、API により、要素の幅、高さ、位置など、遷移を細かく制御できます。こうした高度な遷移は、あるページから次のページへの流れを伝えるうえで非常に役立ちます。
例として写真ギャラリーを考えてみましょう。最もわかりやすい遷移は写真のサイズと位置です。各ページの <img> 要素に同じ一意の view-transition-name と、CSS の containment 値 layout を設定すれば、自動的に実現できます。view-transition-name はスタイル属性に直接記述することも、動的に追加することもできます(たとえば onclick ハンドラー内)。ただし、ページ内で一意であり、遷移の開始前に追加されている必要があります。
写真の下に表示する詳細情報には、もう少しスタイルを設定する必要があります。各行の要素にそれぞれ独自の view-transition-name を設定します。
figcaption h2 {
contain: layout;
view-transition-name: photo-heading;
}
figcaption div {
contain: layout;
view-transition-name: photo-location-time;
}
figcaption dl {
contain: layout;
view-transition-name: photo-meta;
}
これにより、領域ごとに_遷移グループ_が生成されます。これは先ほど説明した新旧のスクリーンショットと同じようなものですが、ドキュメント全体ではなく、ページの一部だけを対象とします。ドキュメント全体の遷移要素を ::view-transition-old(root) と ::view-transition-new(root) で対象にできるのと同様に、これらの遷移グループは ::view-transition-old(NAME) と ::view-transition-new(NAME) で対象にできます。写真グリッドのページには詳細テキストがないため、グリッドから写真ページへ遷移する場合は ::view-transition-new(NAME) のみが存在し、::view-transition-old(NAME) は存在しません。逆方向に移動する場合はその逆です。そのため、:only-child 疑似クラスを使ってこれらのケースを対象にし、アニメーションをカスタマイズできます。photo-heading グループの場合は次のとおりです。
/* Enter */
::view-transition-new(photo-heading):only-child {
animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-up;
}
/* Exit */
::view-transition-old(photo-heading):only-child {
animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-down;
}
API の基本は以上です。Jake Archibald による優れた View Transitions の記事で詳しく解説されています。それでは、ページ全体のナビゲーションをどのように遷移させるか見ていきましょう。
ページ遷移
一般的なページ遷移は、次のような流れになります。
- ユーザーがリンクをクリックする
- データをリクエストする
- レスポンスを使って DOM を更新する
この流れにビュー遷移を適用するには、いくつか考慮すべき点があります。
まず、画面がフリーズした状態になる時間を最小限に抑えることです。ビュー遷移が始まると、コールバックが完了するまで DOM が操作できなくなることに気付いたかもしれません。ユーザーがリンクをクリックした時点で遷移を始めると、UI がフリーズした状態でしばらく待たされる可能性があります。この煩わしさを最小限に抑えるには、リクエストの完了後に document.startViewTransition を呼び出すのが理想的です。そうすれば、変更の準備ができているため、DOM をできるだけ速やかに更新できます。
次に、DOM を更新する前に初期 DOM のスクリーンショットがキャプチャされていることを確認する必要があります。サードパーティ製フレームワークでページ遷移を扱う場合、レンダリング処理を完全には制御できません。レスポンスを受け取ると、DOM は自動的に更新されます。そのため、DOM の更新を適切に実行する関数を document.startViewTransition に渡すことはできません。DOM を更新する単一の関数があるかのように見せるため、レンダリングをインターセプト、一時停止、再開する必要があるかもしれません。
幸い、DOM 更新コールバックから Promise を返すと、ビュー遷移 API はアニメーションを実行する前にその解決を待ちます。この機能を使って、上記のタイミングの問題に対処できます。
React コンポーネントの例
上記の問題に対処するため、関数コンポーネントよりもフローを説明しやすい React クラスコンポーネントを作成します。レンダリングを制御するため、次のライフサイクルメソッドを使用します。
shouldComponentUpdate:ここではfalseを返し、ビュー遷移を開始します。これにより、スクリーンショットのキャプチャが完了するまで時間を確保できますforceUpdate:スクリーンショットのキャプチャ後にコンポーネントを手動で再レンダリングしますcomponentDidUpdate:DOM が更新されたことをビュー遷移 API に通知します
次のようになります。
import { Component } from "react";
export default class ViewTransition extends Component {
shouldComponentUpdate() {
if (!document.startViewTransition) return true; // skip when not supported
document.startViewTransition(() => this.#updateDOM());
return false; // don't update the component, we'll do this manually
}
#updateDOM() {
// now we know the screenshot has been taken, we can force render
// (which skips `shouldComponentUpdate`)
this.forceUpdate();
// set up a promise that will resolve when the component renders
return new Promise((resolve) => {
this.#rendered = resolve;
});
}
render() {
return this.props.children;
}
#rendered = () => {};
componentDidUpdate() {
// resolve the `updateDOM` promise to notify the View Transition API
// that the DOM has been updated
this.#rendered();
}
}
注記: Next.js App Router は執筆時点ではベータ版であり、ベストプラクティスや pages ディレクトリに関する内容は変更される可能性があります。
Next.js アプリでこれを使うには、まず開発環境で React の厳格モードを無効にします。厳格モードでは、コンポーネントを 2 回レンダリングしてチェックを実行します。開発環境では ViewTransition のレンダリングフローに影響するため、厳格モードを全体で無効にし、子コンポーネントでは StrictMode コンポーネントを使って再度有効にします。
// next.config.js
const nextConfig = {
reactStrictMode: false,
};
module.exports = nextConfig;
次に、pages/_app.js で Component を ViewTransition コンポーネントと StrictMode コンポーネントでラップすると、アニメーション付きの遷移が表示されるようになります。
// pages/_app.js
import "@/styles/globals.css";
import { StrictMode } from "react";
import ViewTransition from "@/components/ViewTransition";
export default function App({ Component, pageProps }) {
return (
<ViewTransition>
<StrictMode>
<Component {...pageProps} />
</StrictMode>
</ViewTransition>
);
}
Next.js のデモ、Next.js のライブデモと、そのソースをご覧ください。
注記: React のドキュメントでは、shouldComponentUpdate と forceUpdate の使用は推奨されておらず、パフォーマンス最適化のためだけに使用すべきであるとされています。また、shouldComponentUpdate が呼び出される保証はありません。ページアニメーションは機能強化であり、このコンポーネントは shouldComponentUpdate が呼び出されない場合でも動作するため、この注意点は許容できます。
View Transitions を使わない別のアプローチ
ページ遷移に View Transitions API を使う際の避けられないデメリットは、アニメーションを開始する前に新しいページの HTML が必要になることです。その取得には時間がかかることがあり、ユーザーがリンクをクリックした後、何のフィードバックもない状態が続いてしまいます。その間をスピナーで埋める方法もありますが、リンクがクリックされたらすぐに要素を退場させるアニメーションを開始し、新しい HTML が届いたら登場させるアニメーションを開始することで、その時間を活用できます。これは、次の画面を読み込んでいる間に、標準的な iOS のナビゲーションで画面がすぐにスライドして切り替わる動作に似ています。
- ユーザーがリンクをクリックします
- 要素を退場させるアニメーションを実行する一方で、データをリクエストします
- レスポンスとアニメーションの両方が完了するまで待ちます
- レスポンスを登場させるアニメーションを実行します
このアプローチと View Transitions API のアプローチとの主な違いは、ある状態から次の状態へ要素を_遷移_させられないことです。退場のアニメーションを実行する時点では、そのために必要な新しい HTML がまだないためです。
どちらのアプローチも状況に応じて役立ちます。たとえば、ページ間で共通する要素がある場合は View Transitions を選ぶとよいでしょう。一方、変更が大きく共通する要素が少ない場合は、すぐにフィードバックを返せる退場アニメーションが効果的です。
これを実装するには、ルーティングイベントをフックする必要があります。方法は使用しているフレームワークやライブラリによって異なります。特に、ユーザーがページ遷移したときに通知を受け取る必要があります。Next.js では、routeChangeStart router event を使って退場アニメーションを開始できますが、Next.js、React、またはクライアント側ですべてをレンダリングする HTML を使わずに実現する方法を見ていきましょう。
Turbo と Turn を使用したサーバーサイドレンダリングのマルチページアプリケーションのアニメーション
注:JavaScript を使わずにマルチページナビゲーションを実現できるよう、View Transition API を拡張する計画があります。ただし、より高度なトランジションには、引き続き JavaScript API が必要になる場合があります。
Turbo は、Hotwire ライブラリスイートの一部であり(Vercel's Turbo とは別のものです)、マルチページアプリケーション(MPA)を段階的に強化するレンダリング方式を提供します。コードを完全なクライアントサイドレンダリングのアプリケーションとして設計せずに SPA の速度を実現することを目指しており、リンクのクリックとフォームの送信を捕捉し、JavaScript でリクエストを実行して、レスポンスの新しい <body> で現在の <body> を置き換えます。このように、HTML はサーバーで生成されますが、DOM は JavaScript で更新されるハイブリッド方式です。
Turn は、Turbo を使用してページ遷移をアニメーション化するライブラリです。どちらのアニメーション方式にも対応しています(ただし、現在、View Transitions は実験的な機能です)。Turn は適切なタイミングで <html> 要素に turn-before-exit、turn-exit、turn-enter クラスを追加し、開発者がアニメーションをカスタマイズできるようにします。
これを動作させるには、アニメーション化する要素に data-turn-exit 属性と data-turn-enter 属性を追加し、CSS スタイルを適用します。たとえば、フェードイン/フェードアウトの場合は次のようになります。
html.turn-exit [data-turn-exit] {
animation-name: fade-out;
animation-duration: 0.3s;
animation-fill-mode: forwards;
}
html.turn-enter [data-turn-enter] {
animation-name: fade-in;
animation-duration: 0.6s;
animation-fill-mode: forwards;
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
次に、アプリケーションの JavaScript に Turn ライブラリをインポートし、Turn.start() を呼び出します。
Turbo のレンダリングイベントにフックし、必要に応じて処理の流れを制御することで動作します。
turbo:visit:リクエストの開始直前に、turn-exitクラスを追加しますturbo:before-render:リクエストの完了後、新しい HTML のレンダリング前(React のshouldComponentUpdateに似ています)に、終了アニメーションが完了するまでレンダリングを一時停止しますturbo:render:新しい HTML のレンダリング後、turn-exitクラスを削除して、turn-enterクラスを追加します- 終了アニメーションが完了したら、
turn-enterクラスを削除します
Turn は View Transitions の実験的なサポートも備えており、Turn.config.experimental.viewTransitions = true を設定すると有効になります。View Transitions がサポートされている環境ではそれを使用し、そうでない場合は CSS アニメーション方式にフォールバックします。
まとめ
ページ遷移は、あるページから次のページへの変化を伝える優れた方法です。新しい組み込みの View Transitions API は、遷移前後の状態を渡すことで、複雑なトランジションを実行できます。フレームワークのイベントにフックすることで、これらの状態変化を API に伝えられます。ページナビゲーションでは、DOM が非アクティブな状態になるのを避けるため、リクエストの完了後にトランジションを実行するのが理想的です。
別の方法(または補完的な方法)として、ユーザーがリンクをクリックした直後に、終了アニメーションを実行できます。これにより、新しい HTML が届くまでの間にリクエストが完了する時間を確保できます。