シングルページ アプリケーションでの同一ドキュメント ビュー遷移

1 つのドキュメントに対して実行されるビューの遷移を「同一ドキュメント ビュー遷移」と呼びます。これは通常、JavaScript を使用して DOM を更新するシングルページ アプリケーション(SPA)が当てはまります。Chrome 111 では、同一ドキュメント ビューの移行がサポートされています。

同じドキュメント ビューの遷移をトリガーするには、document.startViewTransition を呼び出します。

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

この関数が呼び出されると、view-transition-name CSS プロパティが宣言されているすべての要素のスナップショットが自動的にキャプチャされます。

次に、渡されたコールバックを実行して DOM を更新します。その後、新しい状態のスナップショットを取得します。

これらのスナップショットは疑似要素のツリーに配置され、CSS アニメーションを活用してアニメーション化されます。新旧の状態からのスナップショットのペアが元の位置とサイズから新しい場所にスムーズに移行し、その一方でコンテンツはクロスフェードします。必要に応じて、CSS を使用してアニメーションをカスタマイズできます。


デフォルトの遷移: クロスフェード

デフォルトのビュー遷移はクロスフェードであるため、API の導入に役立ちます。

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

ここで、updateTheDOMSomehow は DOM を新しい状態に変更します。この処理はお望みのとおりに行うことができます。たとえば、要素の追加や削除、クラス名の変更、スタイルの変更などを行えます。

このように、ページはクロスフェードします。

デフォルトのクロスフェード。最小限のデモソース

クロスフェードはあまりいいじゃない。幸い、遷移はカスタマイズできます。まず、この基本的なクロスフェードの仕組みを理解する必要があります。


移行の仕組み

前のコードサンプルを更新しましょう。

document.startViewTransition(() => updateTheDOMSomehow(data));

.startViewTransition() が呼び出されると、API はページの現在の状態を取得します。これにはスナップショットの作成も含まれます。

完了すると、.startViewTransition() に渡されたコールバックが呼び出されます。ここで DOM が変更されます。その後、API はページの新しい状態を取得します。

新しい状態がキャプチャされると、API は次のような疑似要素ツリーを構築します。

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition は、ページ上の他のものよりも上に重なって配置されます。これは、切り替え効果の背景色を設定する場合に役立ちます。

::view-transition-old(root) は古いビューのスクリーンショット、::view-transition-new(root) は新しいビューのライブ表現です。どちらも CSS の「置き換えられたコンテンツ」としてレンダリングされます(<img> など)。

古いビューは opacity: 1 から opacity: 0 にアニメーション化され、新しいビューは opacity: 0 から opacity: 1 にアニメーション化され、クロスフェードが作成されます。

すべてのアニメーションは CSS アニメーションを使用して実行されるため、CSS でカスタマイズできます。

移行をカスタマイズする

すべてのビュー遷移疑似要素は CSS でターゲットに設定できます。アニメーションは CSS を使用して定義されるため、既存の CSS アニメーション プロパティを使用して変更できます。次に例を示します。

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

1 つの変更で、フェードはかなり遅くなります。

ロング クロスフェード。最小限のデモソース

まだあまり良い印象は持っていません。代わりに、次のコードでマテリアル デザインの共有軸遷移を実装します。

@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); }
}

::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;
}

::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;
}

結果は次のとおりです。

共有軸の遷移。最小限のデモソース

複数の要素を移行

前のデモでは、ページ全体が共有軸の移行に関係しています。ほとんどのページで問題ありませんが、スライドして元に戻るためだけにスライドして表示されるため、見出しには適切ではないようです。

これを回避するには、ページの他の部分からヘッダーを抽出して、個別にアニメーション化できるようにします。これを行うには、view-transition-name を要素に割り当てます。

.main-header {
  view-transition-name: main-header;
}

view-transition-name の値は自由に指定できます(ただし、none は遷移名がないことを意味します)。遷移全体で要素を一意に識別するために使用されます。

その結果、次のようになります。

固定ヘッダーでの共有軸の遷移。最小限のデモソース

これで、ヘッダーはそのままで、クロスフェードします。

この CSS 宣言により、擬似要素ツリーが次のように変更されました。

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

これで移行グループが 2 つになりました。1 つはヘッダー用、もう 1 つはヘッダー用です。これらは CSS と独立してターゲットを設定し、異なる遷移を指定できます。ただし、このケースでは main-header がデフォルトの遷移(クロスフェード)のままです。

デフォルトの遷移は単なるクロスフェードではなく、::view-transition-group による遷移です。

  • 位置と変換(transform を使用)
  • 高さ

ヘッダーのサイズと位置が DOM の両側で同じであるため、これまでは重要ではありませんでした。ただし、ヘッダー内のテキストを抽出することもできます。

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content は、要素がテキストのサイズになるように使用され、残りの幅まで引き伸ばされません。そうしないと、「戻る」矢印によってヘッダーのテキスト要素のサイズが小さくなり、両方のページで同じサイズにはなりません。

操作は 3 つの部分に分かれています。

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

ただし、ここでもデフォルトをそのまま使用します。

スライドするヘッダー テキスト。最小限のデモソース

これで、見出しのテキストが、やや満足のいくスライドで [戻る] ボタンのスペースを作るようになりました。


view-transition-class を使用して、複数の疑似要素を同じ方法でアニメーション化する

対応ブラウザ

  • 125
  • 125
  • x
  • x

多数のカードを使用したビュー遷移があり、ページ上にタイトルもあるとします。タイトル以外のすべてのカードをアニメーション化するには、個々のカードを対象とするセレクタを作成する必要があります。

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

要素が 20 個あるなら、20 のセレクタを記述することになります。新しい要素を追加する場合次に、アニメーション スタイルを適用するセレクタも拡張する必要があります。スケーラビリティは完全ではありません。

ビュー遷移疑似要素で view-transition-class を使用すると、同じスタイルルールを適用できます。

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

次のカードの例では、上記の CSS スニペットを活用しています。新しく追加したカードを含むすべてのカードで、1 つのセレクタ(html::view-transition-group(.card))で同じタイミングが適用されます。

カードデモの録画。view-transition-class を使用すると、追加または削除されたカードを除くすべてのカードに同じ animation-timing-function が適用されます。

遷移をデバッグする

ビュー遷移は CSS アニメーションを基に構築されるため、Chrome DevTools の [アニメーション] パネルは遷移のデバッグに最適です。

[Animations] パネルを使用すると、次のアニメーションを一時停止してから、前後にスクラブできます。その間、[要素] パネルで遷移疑似要素を確認できます。

Chrome DevTools でビュー遷移をデバッグします。

遷移する要素は同じ DOM 要素である必要はない

これまでは、view-transition-name を使用して、ヘッダーとヘッダー内のテキストに別々の遷移要素を作成しました。これらは概念的には DOM 変更前と変更後の要素と同じですが、そうでない場合は遷移を作成できます。

たとえば、メインの動画の埋め込みには view-transition-name を指定できます。

.full-embed {
  view-transition-name: full-embed;
}

次に、サムネイルがクリックされたときに、遷移中のみ同じ view-transition-name を指定できます。

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

結果は次のようになる

ある要素が別の要素に遷移しています。最小限のデモソース

サムネイルがメイン画像に移行します。概念的には(文字どおり)異なる要素であっても、Transition API は同じ view-transition-name を共有しているため、同じものとして扱われます。

この遷移の実際のコードは、上の例よりも少し複雑です。これは、サムネイル ページへの遷移も処理するためです。完全な実装については、ソースをご覧ください


開始と終了のカスタム遷移

次の例をご覧ください。

サイドバーの開始と終了。最小限のデモソース

サイドバーは移行の一部です。

.sidebar {
  view-transition-name: sidebar;
}

ただし、前の例のヘッダーとは異なり、サイドバーはすべてのページに表示されるわけではありません。両方の状態にサイドバーがある場合、遷移疑似要素は次のようになります。

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

ただし、サイドバーが新しいページにのみ配置されている場合、::view-transition-old(sidebar) 疑似要素は表示されません。サイドバーの「古い」イメージはないため、image-pair には ::view-transition-new(sidebar) のみが含まれます。同様に、サイドバーが古いページのみにある場合、image-pair には ::view-transition-old(sidebar) のみが含まれます。

前のデモでは、サイドバーが開始時、終了時、両方の状態で表示されているかによって、サイドバーの遷移が異なります。右からスライドしてフェードインし、右にスライドしてフェードアウトして出て、両方の状態にあるときは同じ位置に留まります。

特定の開始遷移と終了遷移を作成するには、:only-child 疑似クラスを使用して、画像ペアの唯一の子である新旧の疑似要素をターゲットにします。

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

この場合、デフォルトが完全であるため、サイドバーが両方の状態に存在する場合の特定の遷移はありません。

非同期 DOM 更新とコンテンツの待機

.startViewTransition() に渡されるコールバックは Promise を返すことができます。これにより、非同期の DOM 更新が可能になり、重要なコンテンツの準備が整うのを待機できます。

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

Promise が完了するまで、移行は開始されません。この間、ページは凍結されるため、遅延を最小限に抑える必要があります。具体的には、ネットワーク フェッチは、.startViewTransition() コールバックの一部として実行するのではなく、ページが完全にインタラクティブである間に、.startViewTransition() を呼び出す前に行う必要があります。

画像やフォントの準備が整うまで待つ場合は、次のような積極的なタイムアウトを使用してください。

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

ただし、場合によっては、遅延を完全に回避し、既存のコンテンツを使用したほうがよい場合もあります。


既存のコンテンツを最大限に活用する

サムネイルが大きい画像に切り替わる場合:

大きな画像に切り替わるサムネイル。デモサイトをお試しください

デフォルトの遷移はクロスフェードです。つまり、まだ読み込まれていないフル画像とサムネイルがクロスフェードする可能性があります。

これに対処する 1 つの方法は、画像全体が読み込まれるのを待ってから遷移を開始することです。この処理は .startViewTransition() を呼び出す前に行うことが理想的です。そうすることで、ページはインタラクティブな状態を保ち、読み込み中であることをユーザーに知らせるスピナーを表示できます。しかし、この場合はもっと良い方法があります。

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

サムネイルはフェードアウトせず、画像全体の下に表示されるようになりました。つまり、新しいビューが読み込まれていない場合は、遷移中もサムネイルが表示されます。つまり、遷移がすぐに開始され、適切なタイミングで画像全体を読み込むことができます。

新しいビューで透明度が設定されている場合はうまくいきませんが、今回はそうでないことがわかっているため、この最適化を行います。

アスペクト比の変更を処理する

これまでのところ、すべての遷移は同じアスペクト比の要素に対して行われていましたが、必ずしもそうとは限りません。サムネイルが 1:1、メイン画像が 16:9 の場合はどうでしょうか。

アスペクト比を変えて別の要素に遷移する要素。最小限のデモソース

デフォルトの遷移では、グループは変更前のサイズから後のサイズにアニメーション化されます。古いビューと新しいビューはグループの 100% の幅で、高さは自動設定されます。つまり、グループのサイズに関係なくアスペクト比が維持されます。

これは良いデフォルトですが、今回のケースでは必要ありません。それによって次のようになります。

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

つまり、幅を広げてもサムネイルは要素の中央に留まりますが、1:1 から 16:9 に変化しても画像全体が「切り抜き解除」されます。

詳しくは、ビュー遷移: アスペクト比の変更の処理をご覧ください。


メディアクエリを使用して、デバイスの状態に応じて遷移を変更する

モバイルとパソコンでは異なる切り替え効果を使用できます。次の例では、モバイルでは横からスライドしていますが、パソコンではより繊細なスライドを使用しています。

ある要素が別の要素に遷移しています。最小限のデモソース

これを行うには、通常のメディアクエリを使用します。

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::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;
  }

  ::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;
  }
}

一致するメディアクエリに応じて、view-transition-name を割り当てる要素を変更することもできます。


「動きの抑制」の設定に反応する

ユーザーは、オペレーティング システムでモーションの抑制を好むことを示せます。その設定は CSS で公開されます。

以下のユーザーが移行されないようにすることもできます。

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

ただし、「動きの軽減」が望ましいということは、ユーザーが「動きがない」という意味ではありません。前述のスニペットの代わりに、より繊細なアニメーションでも、要素間の関係とデータの流れを表現したものにすることもできます。


ビュー遷移タイプを使用して複数のビュー遷移スタイルを処理する

ある特定のビューから別のビューへの移行では、特別にカスタマイズされた移行が必要になる場合があります。たとえば、ページ分けの順序内で次または前のページに移動する場合、移動順序の上位または下位のページのどちらに移動するかに応じて、コンテンツを異なる方向にスライドさせることができます。

ページネーションのデモの録画。表示するページに応じて、切り替え方法が異なります。

そのためには、ビュー遷移タイプを使用できます。ビュー遷移タイプを使用すると、アクティブ ビュー遷移に 1 つ以上のタイプを割り当てることができます。たとえば、ページ分けシーケンスで上位のページに移動する場合は forwards タイプを使用し、下位ページに移動する場合は backwards タイプを使用します。これらのタイプは、遷移をキャプチャまたは実行する場合にのみアクティブになります。また、各タイプは CSS でカスタマイズして、さまざまなアニメーションを使用できます。

同一ドキュメント ビュー遷移で型を使用するには、typesstartViewTransition メソッドに渡します。これを可能にするために、document.startViewTransition はオブジェクトも受け入れます。update は DOM を更新するコールバック関数で、types は型を含む配列です。

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

これらのタイプに応答するには、:active-view-transition-type() セレクタを使用します。ターゲットにする type をセレクタに渡します。これにより、一方の宣言がもう一方の宣言に干渉することなく、複数のビュー遷移のスタイルを互いに分離した状態に保つことができます。

タイプは遷移をキャプチャまたは実行する場合にのみ適用されるため、セレクタを使用して、そのタイプのビュー遷移に対してのみ、要素の view-transition-name を設定または設定解除できます。

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

次のページ分けのデモでは、移動先のページ番号に基づいてページ コンテンツが前後にスライドします。型はクリックによって決定され、document.startViewTransition に渡されます。

タイプに関係なく、アクティブ ビュー遷移をターゲットにするには、代わりに :active-view-transition 疑似クラスセレクタを使用します。

html:active-view-transition {
    …
}

ビュー遷移ルートのクラス名を使用して複数のビュー遷移スタイルを処理する

ある特定のタイプのビューから別のタイプのビューに移行する場合は、特別にカスタマイズされた移行を行う必要があります。または、「戻る」ナビゲーションと「進む」ナビゲーションは異なる必要があります。

「戻る」ときの移動方法の違い。最小限のデモソース

「遷移タイプ」が導入される前は、これらのケースを処理する方法は、遷移ルートに一時的にクラス名を設定することでした。document.startViewTransition を呼び出す場合、この遷移ルートは <html> 要素であり、JavaScript で document.documentElement を使用してアクセスできます。

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

この例では、遷移の完了後にクラスを削除するために transition.finished を使用しています。これは、遷移が終了状態に達すると解決される Promise です。このオブジェクトのその他のプロパティについては、API リファレンスをご覧ください。

これで、CSS でクラス名を使用して、遷移を変更できるようになりました。

/* 'Forward' transitions */
::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;
}

::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;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

メディアクエリと同様に、これらのクラスを使用することで、view-transition-name を取得する要素を変更することもできます。


他のアニメーションをフリーズせずに遷移を実行する

動画の切り替え位置のデモをご覧ください。

動画切り替え。最小限のデモソース

何か問題がありましたか?心当たりがない場合でもご心配なく。ここでは速度が低下します。

動画切り替え、低速。最小限のデモソース

動画の切り替え中は、動画がフリーズしたように見えた後、再生中の動画がフェードインします。これは、::view-transition-old(video) が古いビューのスクリーンショットであるのに対し、::view-transition-new(video) は新しいビューのライブ画像であるためです。

この問題は修正できますが、まずは修正する価値があるかどうかを確認してください。切り替え効果が通常の速度で再生されているときに「問題」が見つからなかった場合は、変更する必要はありません。

本当に修正する必要がある場合は、::view-transition-old(video) を表示せず、::view-transition-new(video) に直接切り替えてください。これを行うには、デフォルトのスタイルとアニメーションをオーバーライドします。

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

これで完了です。

動画切り替え、低速。最小限のデモソース

これで、遷移中に動画が再生されます。


JavaScript によるアニメーション化

ここまでのところ、すべての遷移は CSS を使用して定義されていますが、CSS では不十分な場合もあります。

円の移行。最小限のデモソース

この移行の一部は、CSS だけでは実現できません。

  • アニメーションはクリックした位置から開始します。
  • アニメーションは、円の最も遠い隅を半径として終了します。ただし、将来的に CSS で可能になることを願っています。

Web Animation API を使用すると、遷移を作成できます。

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

この例では、遷移疑似要素が正常に作成されたときに解決される Promise である transition.ready を使用します。このオブジェクトのその他のプロパティについては、API リファレンスをご覧ください。


拡張機能としてのトランジション

View Transition API は、DOM の変更を「ラップ」し、それに対する遷移を作成するように設計されています。ただし、DOM の変更は成功しても遷移に失敗しても、アプリが「エラー」状態にならないように、拡張として扱う必要があります。移行が失敗しないのが理想的ですが、失敗しても、残りのユーザー エクスペリエンスが損なわれることはありません。

遷移を機能強化として扱うため、遷移 Promise を、遷移が失敗した場合にアプリがスローするような方法で使用しないように注意してください。

すべきでないこと
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

この例の問題は、遷移が ready 状態に到達できない場合に switchView() が拒否されることですが、これはビューの切り替えに失敗したという意味ではありません。DOM は正常に更新された可能性がありますが、view-transition-name が重複しているため、遷移はスキップされました。

その場合は次の方法を試してください。

推奨事項
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

この例では、transition.updateCallbackDone を使用して DOM の更新を待機し、更新が失敗した場合は拒否します。switchView は、遷移に失敗しても拒否されなくなり、DOM の更新が完了すると解決され、失敗した場合は拒否されるようになりました。

アニメーションによる遷移が完了または最後までスキップされた場合など、新しいビューが「整合」したときに switchView を解決する場合は、transition.updateCallbackDonetransition.finished に置き換えます。


ポリフィルではないけど...

この対象物をポリフィルするのは簡単ではありません。ただし、このヘルパー関数を使用すると、ビュー遷移をサポートしていないブラウザでも処理がはるかに簡単になります。

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

次のように使用できます。

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

ビュー遷移をサポートしていないブラウザでも updateDOM は呼び出されますが、アニメーションによる遷移は表示されません。

また、遷移中に <html> に追加する classNames を指定して、ナビゲーションの種類に応じて遷移を変更しやすくすることもできます。

ビュー遷移をサポートしているブラウザであっても、アニメーションが不要な場合は、trueskipTransition に渡すこともできます。この方法は、サイトの切り替えを無効にすることをユーザーが希望する場合に役立ちます。


フレームワークの操作

DOM の変更を抽象化するライブラリやフレームワークを使用している場合は、DOM の変更が完了したかどうかを把握することが難しい部分です。さまざまなフレームワークで上記のヘルパーを使用した例を示します。

  • React - ここでのキーは flushSync で、一連の状態の変更を同期して適用します。はい、その API の使用については大きな警告がありますが、この場合は Dan Abramov が使用に適していると断言しています。React や非同期コードの場合と同様に、startViewTransition から返されるさまざまな Promise を使用する場合は、コードが正しい状態で実行されているように注意してください。
  • Vue.js - ここでのキーは nextTick で、DOM が更新されると実行されます。
  • Svelte - Vue によく似ていますが、次の変更を待機するメソッドは tick です。
  • Lit - ここで重要なのは、コンポーネント内の this.updateComplete Promise で、これは DOM が更新されると実現されます。
  • Angular - ここでのキーは applicationRef.tick で、保留中の DOM 変更がフラッシュされます。Angular バージョン 17 以降では、@angular/router に付属する withViewTransitions を使用できます。

API リファレンス

const viewTransition = document.startViewTransition(update)

新しい ViewTransition を開始します。

update は、ドキュメントの現在の状態がキャプチャされたときに呼び出される関数です。

その後、updateCallback によって返された Promise が満たされると、次のフレームで遷移が開始されます。updateCallback から返された Promise が拒否された場合、遷移は放棄されます。

const viewTransition = document.startViewTransition({ update, types })

指定されたタイプで新しい ViewTransition を開始します。

ドキュメントの現在の状態がキャプチャされると、update が呼び出されます。

types は、遷移をキャプチャまたは実行する際に、遷移に対してアクティブなタイプを設定します。最初は空です。詳しくは、下の viewTransition.types をご覧ください。

ViewTransition のインスタンス メンバー:

viewTransition.updateCallbackDone

updateCallback から返された Promise が履行されたときに履行される Promise。またはリジェクションが却下されると拒否される Promise。

View Transition API は、DOM の変更をラップして遷移を作成します。しかし、トランジション アニメーションが成功するか失敗するかは気にせず、DOM の変化がいつ、起きたかだけを知りたい場合もあります。updateCallbackDone はそのユースケースに適しています。

viewTransition.ready

遷移用の疑似要素が作成され、アニメーションが開始されようとしたときに実行される Promise。

遷移を開始できない場合、拒否されます。これは、構成ミス(view-transition-name の重複など)や、updateCallback が拒否された Promise を返すことが原因である可能性があります。

これは、JavaScript で遷移疑似要素をアニメーション化する場合に便利です。

viewTransition.finished

最終状態が完全に表示され、ユーザー操作可能になると解決される Promise。

updateCallback が拒否された Promise を返す場合にのみ拒否されます。これは、終了状態が作成されていないことを意味するからです。

遷移の開始に失敗した場合や、遷移中にスキップされた場合でも、終了状態になるため、finished が満たされます。

viewTransition.types

アクティブ ビュー遷移のタイプを保持する Set のようなオブジェクト。エントリを操作するには、そのインスタンス メソッド clear()add()delete() を使用します。

CSS で特定のタイプに応答するには、遷移ルートで :active-view-transition-type(type) 疑似クラス セレクタを使用します。

ビュー遷移が完了すると、型が自動的にクリーンアップされます。

viewTransition.skipTransition()

遷移のアニメーション部分をスキップします。

DOM の変更は遷移とは別であるため、updateCallback の呼び出しはスキップされません。


デフォルトのスタイルと切り替え効果のリファレンス

::view-transition
ビューポート全体に表示され、各 ::view-transition-group を含むルート疑似要素。
::view-transition-group

確実な位置付け。

「変更前」状態と「変更後」状態の間での widthheight の遷移。

ビューポート空間のクワッド「before」と「after」の間で transform を移行します。

::view-transition-image-pair

グループを埋めるのに絶好のポジショニングです。

isolation: isolate を使用して、古いビューと新しいビューに対する mix-blend-mode の効果を制限します。

::view-transition-new::view-transition-old

ラッパーの左上に必ず配置されます。

グループの幅の 100% に表示されますが、高さが自動的に設定されるため、グループを埋めるのではなくアスペクト比を維持します。

真のクロスフェードを可能にする mix-blend-mode: plus-lighter を持ちます。

古いビューは opacity: 1 から opacity: 0 に移行します。新しいビューは opacity: 0 から opacity: 1 に移行します。


フィードバック

デベロッパーからのフィードバックを常に歓迎いたします。そのためには、提案や質問を含めて GitHub の CSS ワーキング グループに問題を提出してください。問題の接頭辞に [css-view-transitions] を付けます。

バグが発生した場合は、代わりに Chromium のバグを報告してください。