CSS と疑似要素

View Transition のカスタマイズはほぼ全部 CSS 側で行う。 名前を付けて、疑似要素を @keyframes でターゲットする、これさえ覚えれば応用が利く。

疑似要素ツリーの全体像

transition が始まると、ブラウザは以下のツリーを ::view-transition として ドキュメントの最前面(::backdrop の上)に組み立てる。

::view-transition                          ← 全体のルート(fixed, inset:0)
└─ ::view-transition-group(root)           ← グループ。位置/サイズを補間
   └─ ::view-transition-image-pair(root)   ← old/new を重ねる箱(mix-blend)
      ├─ ::view-transition-old(root)       ← 古いスナップショット画像(fade-out)
      └─ ::view-transition-new(root)       ← 新しいスナップショット画像(fade-in)

# view-transition-name を付けた要素ごとに同じ階層が増える
└─ ::view-transition-group(card-3)
   └─ ::view-transition-image-pair(card-3)
      ├─ ::view-transition-old(card-3)
      └─ ::view-transition-new(card-3)

各階層の役割

疑似要素担当
::view-transition全体のオーバーレイ。普段はいじらない。
::view-transition-group(name)位置・サイズの補間を担当。width/height/transform がここに animation として乗る。
::view-transition-image-pair(name)old と new を重ねる箱。基本は isolation: isolate で blend を分離する用途。
::view-transition-old(name)古いスナップショット画像。デフォルトで fade-out。
::view-transition-new(name)新しいスナップショット画像。デフォルトで fade-in。

view-transition-name

ある要素を独立した snapshot ペアとして扱うためのフラグ。 値は文字列の識別子(または none / auto)。 遷移前後で同じ名前の要素はペアにされ、位置とサイズが補間される

CSS
.card-hero {
  view-transition-name: card-hero;
}
名前衝突は transition のスキップ原因 No.1

リストに対して .card { view-transition-name: card; } のように静的に同じ名前を当てると、 要素が複数になった瞬間に名前衝突で transition 全体がスキップされる。
リストには auto か、style="view-transition-name: card-3" のようにidベースで動的に付ける。

デフォルトのアニメーション

ユーザー側で何も書かなくても、ブラウザは以下を勝手にやる:

カスタマイズ — 期間・カーブだけ変える

::view-transition-group(*) {
  animation-duration: 400ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}

(*) はワイルドカードで、すべての group をターゲットにする。 個別に対象を絞る時は ::view-transition-group(card-hero) のように名前を渡す。

カスタマイズ — アニメーション自体を差し替える

::view-transition-old/new普通の要素として扱える。 @keyframes を書いて animation を上書きすればいい。

スライドイン / スライドアウト
@keyframes slide-out-left {
  to { transform: translateX(-30%); opacity: 0; }
}
@keyframes slide-in-right {
  from { transform: translateX(30%); opacity: 0; }
}

::view-transition-old(root) {
  animation: slide-out-left 300ms ease both;
}
::view-transition-new(root) {
  animation: slide-in-right 300ms ease both;
}
old と new は同時に animate される

old を後ろに残し、new を上から重ねる、というような演出は両方に animation を当てて 同じ duration を使うのが基本。
片方を「先に消えて」「後から出てくる」順序にしたい時は animation-delay で時間差をつける。

カスタマイズ — モーフ(位置・サイズ補間)

view-transition-name を付けた要素は、デフォルトで group(name) に 位置・サイズの補間アニメが乗る。クロスフェード前提のままだと違和感がある場合は、 image-pair 配下の old/new の不透明度を弄って繋ぎ方を作り直すのが定石。

単純な「形は補間、見た目は new だけ」
::view-transition-old(card-hero) {
  /* 古い画像は描かない(位置補間は group が引き続きやる) */
  animation: none;
  opacity: 0;
}
::view-transition-new(card-hero) {
  animation: none;
  opacity: 1;
}

::view-transition-group() を直接動かす

位置補間そのものを書き直すこともできる。ただし width/height/transform を全部自分で制御するのは大変なので、 実用は「色だけ動かす」「最後に少しオーバーシュートする」程度の補正に留めるのが楽。

::view-transition-group(card-hero) {
  animation-duration: 500ms;
  animation-timing-function: cubic-bezier(.16, 1.25, .3, 1); /* overshoot */
}

view-transition-class(Level 2)

Level 2 で導入された、疑似要素をクラス的に一括ターゲットするためのプロパティ。 対応ブラウザでは複数要素に同じ class を付けて、CSS 側で ::view-transition-group(.card-class) のように指定できる。

.card { view-transition-class: card; }

::view-transition-group(.card) {
  animation-duration: 350ms;
}

対応していないブラウザでは個別に view-transition-name を当てる必要があるので、 注意点・ブラウザ対応 を見てから使う。

:active-view-transition / :active-view-transition-type()(Level 2)

進行中の transition の types を見て CSS を分岐できる擬似クラス。 「進む」「戻る」で別アニメにしたいときに使う。

document.startViewTransition({
  update: () => navigate(),
  types: ["forward"],
})
html:active-view-transition-type(forward) {
  &::view-transition-old(root) { animation: slide-out-left 300ms; }
  &::view-transition-new(root) { animation: slide-in-right 300ms; }
}
html:active-view-transition-type(back) {
  &::view-transition-old(root) { animation: slide-out-right 300ms; }
  &::view-transition-new(root) { animation: slide-in-left 300ms; }
}

prefers-reduced-motion の取り扱い

デフォルトのフェード自体は控えめだが、独自に派手な演出を当てている場合は必ず減衰版を用意する

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

完全に止めると遷移瞬間で「カチッ」と切り替わる。
必要なら短いフェードだけは残すのもあり。