ビュートランジションをいろいろと試したことがあるなら、2つのページ間の3Dトランジション(すなわち、クロスドキュメントビュートランジション)がうまく動作しないことに気づいたかもしれません。つまり、ブラウザがまず要素を平坦化しない限りは動作しないということです。

画像要素はこれを説明するのに最適な例です。ビュートランジションの前後状態のスナップショットをブラウザが取得するのと同じように、画像は置換要素であるため、理論上は3Dアニメーションの簡易テストケースとして使用できるはずです。たとえば、クリック時に1つの画像を反転させて別の画像を表示する例は次のようになります。

アニメーションを正しく動作させるためには、画像の親コンテナ(この場合は.scene要素)にperspectiveプロパティを設定する必要があります。そうしないと、3D変換は単に平坦なものになります。要素の見た目をある程度傾けるような感じになります。

CSSでは、親要素のperspective自身を除くすべての子要素に適用されます。

.scene {
  perspective: 1200px;

  .card { /* perspective が適用される */ }
}

ここで重要なのはHTML構造です。特に.sceneコンテナが子要素である.cardの上に配置されることで、3D効果が実現し、フリップが期待通りに見えるようになります。

<div class="scene">
  <div class="card">
    <!-- カードコンテンツはここ -->
  </div>
</div>

おそらく.cardsを反転させるキーフレームアニメーションは次のようになるでしょう。

@keyframes flipOut {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(-90deg);
  }
}

これを.cardsに次のように適用します。

.card.flip-out {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.card.flip-in {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards reverse;
}

…ここで、JavaScriptがクリックを監視して.flip-outクラスを.cardに追加するとアニメーションがforwardsで実行され、.flip-inクラスを追加するとreverseで実行されます。

これはクロスドキュメントビュートランジションが動作する仕組みと同じはずですよね?画像が3Dアニメーションをサポートするなら、ビュートランジションのスナップショットも同じように動作するはずです。では、それを検証してみましょう。

ビュートランジションの設定

まず最初に、両方のページで@view-transitionアットルールを使用してnavigation記述子をautoに設定し、ビュートランジションを有効にする必要があります。

@view-transition {
  navigation: auto;
}

他に何も行わなければ、2つのページ間を移動すると1つのページがフェードインします。これは最も基本的なクロスドキュメントビュートランジションです。

どうやってカスタマイズするのでしょうか?::view-transition-old()および::view-transition-new()擬似クラスを使用します。前者は「古い」スナップショット、後者は「新しい」スナップショットです。先ほどの例で使用した.card要素のように、ここでキーフレームアニメーションを設定します。

::view-transition-old(root) {
  /* アニメーションをここに記述 */
}

::view-transition-new(root) {
  /* アニメーションをここに記述 */
}

rootパラメータは、ビュートランジションがページ全体と、ビュートランジションのデフォルトスナップショットグループによって作成される(および作成されない)すべての要素を対象にすることを指定します。

問題点

「古い」ページのスナップショットが「新しい」ページに反転するように、ウェブページ全体に同じ3Dフリップを適用したいとしましょう。3Dアニメーションには2つの要件があります。

  1. 子要素に3D効果を適用するための親要素へのperspectiveプロパティ
  2. ビュートランジションが発生したときのページへのアニメーション

しかし、ここでperspectiveを設定する対象、つまり親要素とは具体的に何でしょうか?

ビュートランジションはウェブページ全体のスナップショットを取得するため、論理的に考えて<html>要素(または:root)を対象にすると考えるかもしれません。ビュートランジションが存在するときのDOMツリーは次のようになります。

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  └─ ::view-transition-group(name)
  │     └─ ::view-transition-image-pair(name)
  │        ├─ ::view-transition-old(name)
  │        └─ ::view-transition-new(name)
  ├─ head
  └─ body
        └─ …

したがって、perspectiveはスナップショット全体に設定すべきですよね?実際には違います。

実際、何も起こりません!先ほどのカードで実現できた美しい3Dフリップの代わりに、次のような結果になります。

GitHub SourceLive Demo

私が使用していたコードは次のとおりです。

/* クロスドキュメントビュートランジションのオプトイン */
@view-transition {
  navigation: auto;
}

/* 3Dフリップ: 古いページが反転して消え、新しいページが反転して表示 */
@keyframes flip-out {
  0% {
    transform: rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: rotateY(-90deg);
    opacity: 0;
  }
}

@keyframes flip-in {
  0% {
    transform: rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: rotateY(0deg);
    opacity: 1;
  }
}

::view-transition-old(root) {
  animation: flip-out 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
  transform-origin: center center;
}

::view-transition-new(root) {
  animation: flip-in 0.3s cubic-bezier(0, 0, 0.6, 1) 0.3s backwards;
  transform-origin: center center;
}

注: ここではアニメーションを反転させていません。なぜなら-90degに反転してから90degから開始するためです。全く同じではありません!

そして、これはperspectivehtmlまたは:rootのどちらに設定しても動作しません。

/* 👎 */
h
  perspective: 1100px;
}

/* 👎 */
:root {
  perspective: 1100px;
}

調査したところ、perspective(および3D変換全般)は、通常とは異なる効果を生むCSSプロパティの1つであることがわかりました。(答えを持っているのはBramusに任せましょう!)

では、どうすればよいのでしょうか?いくつかアイデアが浮かびましたが、残念ながら失敗しました。

  • bodyperspectiveプロパティを設定してみました。
  • ::view-transition-group(root)内にperspectiveを設定してみました。
  • ::view-transition擬似要素内にperspectiveを設定してみました。

実はこの問題に対する非常にシンプルな回避策があり、なぜ今まで気づかなかったのか不思議です — perspectiveを全く使用しないことです!

解決策

結論から言うと、perspectiveプロパティではなくperspective()関数を使用する必要があります。そして、予想されるように::view-transition-*擬似要素のいずれかの中ではなく、@keyframesアニメーションの中に記述します。

@keyframes flip-out {
  0% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: perspective(1100px) rotateY(-90deg);
    opacity: 0;
  }
}
@keyframes flip-in {
  0% {
    transform: perspective(1100px) rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
}

このシンプルですが大きな変更により、平坦でなんともない状態から美しいああ、いいね状態に変わります。

GitHub SourceLive Demo

理由は次のとおりです。ビュートランジションの擬似要素ツリーは通常のHTMLフローでレンダリングされます。より具体的には、ビュートランジションのツリー全体が独自のレイヤーとしてDOMの上にレンダリングされます。ただし、特に::view-transitionについては理由がよくわかりませんが、私の最善の推測は、各ビュートランジショングループが自動的に位置と変形の値をブラウザによって上書きされるため、perspectiveに干渉するということです。

perspectiveperspective()の違いは何でしょうか?perspectiveプロパティは親要素に適用されますが、perspective()は要素自体に直接適用されるtransformプロパティの関数です。ビュートランジションの擬似ツリーには真の親要素が存在しないため、親要素を必要としないperspective()を使用する必要があります。ふう。

まとめると…

html:root、またはビュートランジションの擬似クラスのいずれかにperspectiveを設定しても動作しません。もし私と同じように解決策を見つけるのに苦労しているなら、この小さくも大きなperspective()の変更が、遭遇したときに問題を解決してくれるでしょう。私から言わせてもらえば、何週間もこの問題と格闘した後、今日これを不満を述べるために戻ってきたことで解決策を発見しました。書くことの利点ですね。