CSSの@view-transitionでページ移動に動きを付ける

​​CSS​HTML,Playwright,View Transition API

CSS に @view-transition { navigation: auto; } と書くと、同じオリジンの別のページへリンクで移るときに、前の画面から次の画面へ動きを付けられます。JavaScript は要りません。ふつうの HTML のページどうしでも、一覧の写真が詳細ページの大きな写真へ広がる、といった動きを作れます。

css
@view-transition {
  navigation: auto;
}
補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。検証環境の中に立てたサーバーで、一覧(index.html)と猫ごとの詳細(cat-1.html など)の2種類のページを作り、Playwright で Chromium を操作しました。動いたかどうかは、移った先のページの pagereveal イベントと document.getAnimations() で調べています。Firefox や Safari、スマホの実機、本物のアドレスバーやブックマークからの移動は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

移る前と移った先の、両方のページに書く

MDN の @view-transition のページにあるとおり、ページ間のビュー遷移は、移る前と移った先のページが同じオリジンにあるときだけ動きます。そのうえで、両方のページに @view-transition を書いておく必要があります。

検証環境で、両方のページに冒頭の CSS を書き、一覧のリンクをクリックしました。移った先のページでは、次のアニメーションが動きました。

text
::view-transition-group(root)  -ua-view-transition-group-anim-root  250ms
::view-transition-new(root)  -ua-view-transition-fade-in  250ms
::view-transition-old(root)  -ua-view-transition-fade-out  250ms

::view-transition-old(root) は移る前の画面のスナップショットで、::view-transition-new(root) は移った先の画面です。前の画面が薄くなって消え、次の画面が浮かび上がるクロスフェードで、0.25 秒かかります。

片方のページにだけ書いた場合も試しました。一覧にだけ書いた場合も、詳細にだけ書いた場合も、動きは付きませんでした。

動きをスライドに変える

動きの中身は、::view-transition-old(root) と ::view-transition-new(root) に、ふつうの CSS アニメーションを当てて変えます。前の画面を左へ出し、次の画面を右から入れる例です。

css
@view-transition {
  navigation: auto;
}

@keyframes slide-out {
  to { translate: -100% 0; }
}
@keyframes slide-in {
  from { translate: 100% 0; }
}

::view-transition-old(root) {
  animation: 400ms ease both slide-out;
}
::view-transition-new(root) {
  animation: 400ms ease both slide-in;
}

検証環境では、::view-transition-old(root) で slide-out が、::view-transition-new(root) で slide-in が、どちらも 400ms で動きました。この CSS も、両方のページに書きます。

一覧の写真を、詳細ページの大きな写真へ移す

移る前と移った先の要素に、同じ view-transition-name を付けると、その要素は画面全体とは別に動きます。一覧の小さな写真と、詳細ページの大きな写真に同じ名前を付けると、写真が元の位置から大きくなりながら移ります。

html
<!-- index.html(一覧) -->
<a href="cat-1.html">
  <div class="thumb" style="view-transition-name: cat-1"></div>
  <p>みけ</p>
</a>

<!-- cat-1.html(詳細) -->
<div class="hero" style="view-transition-name: cat-1"></div>

画面の幅を 400px にした検証環境では、::view-transition-group(cat-1) のアニメーションが、幅 160px・高さ 160px から、幅 368px・高さ 276px へ変わりました。位置も、一覧の写真があった場所から、詳細の写真の場所へ動きました。名前を付けていない部分は、画面全体(root)のクロスフェードの中で動きます。名前を付けた「くろ」の写真のように、移った先に同じ名前の要素がないものは、::view-transition-old(cat-2) だけが薄くなって消えました。MDN の View Transition API の使い方のページにも、::view-transition-group は前と後のあいだで幅と高さを滑らかに変えると書かれています。

次の GIF は、この動きを見やすくするため、すべてのアニメーションの時間を 1.6 秒に延ばして撮ったものです。

css
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 1.6s;
}
猫の一覧で「みけ」の写真をクリックすると、写真が詳細ページの大きな写真へ広がりながら移り、「一覧に戻る」をクリックすると、大きな写真が一覧の小さな写真へ縮みながら戻る
写真のクリックで詳細へ、「一覧に戻る」のクリックで一覧へ。写真が広がったり縮んだりしながら移る(1.6 秒に延ばしています)

一覧に猫が何匹もいるときは、cat-1、cat-2 のように、写真ごとに違う名前を付けます。同じページの中で2つの要素に同じ名前を付けると、動きは付きません。一覧の写真すべてに view-transition-name: cat-photo を付けて試したところ、コンソールに Unexpected duplicate view-transition-name: cat-photo と出て、ページはそのまま切り替わりました。

動きが付く移り方と、付かない移り方

@view-transition を書いても、すべての移り方で動きが付くわけではありません。検証環境で、移り方を変えて試した結果です。

移り方 動き
ページのリンクをクリック 付く
スクリプトで location.href を変える 付く
ブラウザーの「戻る」 付く
再読み込み 付かない
Playwright の page.goto()(ページの中のリンクやスクリプトを使わずに URL を開く) 付かない
別のオリジン(localhost から 127.0.0.1)へのリンク 付かない

仕様の navigation 記述子の説明では、動きが付くのは、同じオリジンへの移動で、途中で別のオリジンへのリダイレクトがなく、移動の種類が traverse(戻る・進む)か、アドレスバーなどブラウザー側の画面から始めたのではない push・replace のときです。アドレスバーへの入力やブックマークからの移動と、再読み込みは除かれると書かれています。

種類を付けて、動きを使い分ける

@view-transition の types に名前を書くと、そのページが関わる遷移に「種類」を付けられます。種類は :active-view-transition-type() で選べるので、種類ごとに動きを書き分けられます。

css
@view-transition {
  navigation: auto;
  types: slide;
}

html:active-view-transition-type(slide) {
  &::view-transition-old(root) { animation: 400ms ease both slide-out; }
  &::view-transition-new(root) { animation: 400ms ease both slide-in; }
}

検証環境では、移る前と移った先の両方で、遷移の種類が ["slide"] になり、スライドの動きが付きました。types: slide forward のように空白で区切ると、["slide", "forward"] の2つが付きました。

types は、書いたページの側にしか効きません。一覧にだけ types: slide を書き、詳細には書かずに試すと、詳細のページでは種類が空になり、ふつうのクロスフェードになりました。仕様の types 記述子の説明にも、types は定義した文書にだけ当てはまり、両方の文書で同じ種類を使うのは作る側の役目だと書かれています。

ブラウザーの「戻る」「進む」のときだけ逆向きにする

移った先のページで、どんな移り方で来たかを見て種類を足すこともできます。移動の種類が traverse(ブラウザーの「戻る」「進む」)のときだけ back を足し、スライドの向きを逆にする例です。

html
<script>
  addEventListener('pagereveal', (e) => {
    if (e.viewTransition && window.navigation?.activation?.navigationType === 'traverse') {
      e.viewTransition.types.add('back');
    }
  });
</script>
css
@keyframes slide-out-back {
  to { translate: 100% 0; }
}
@keyframes slide-in-back {
  from { translate: -100% 0; }
}

html:active-view-transition-type(back) {
  &::view-transition-old(root) { animation-name: slide-out-back; }
  &::view-transition-new(root) { animation-name: slide-in-back; }
}

pagereveal は、ページが最初に表示されるときに起きるイベントで、e.viewTransition から遷移を操作できます。MDN の pagereveal のページによると、ページ間の遷移がないときは null です。検証では、このスクリプトを <head> の中に置きました。

ブラウザーの「戻る」で一覧へ戻ると、種類が ["back"] になり、slide-out-back と slide-in-back が動きました。詳細ページの「一覧に戻る」リンクで戻った場合は、移動の種類が push なので back は付かず、一覧から詳細へ移るときと同じ向きのスライドになりました。また、traverse は「戻る」と「進む」の両方を指すため、このコードでは「進む」で来たときにも back が付きます。「戻る」と「進む」を区別したいときや、リンクで戻るときも逆向きにしたいときは、navigation.activation.from と navigation.activation.entry の URL を比べて決める書き方が、MDN の遷移の種類のページに載っています。

動きを減らす設定のときは止める

@view-transition は @media の中に書けます。prefers-reduced-motion と組み合わせると、端末で動きを減らす設定にしている人には、動きを付けないようにできます。

css
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Playwright で prefers-reduced-motion を reduce にして試すと、リンクをクリックしても動きは付きませんでした。no-preference のときは、ふつうにクロスフェードしました。

対応しているブラウザー

互換性データでの対応状況は、次のとおりです。

機能 Chrome・Edge Firefox Safari(iOS を含む)
@view-transition 126 から 対応していない 18.2 から
pagereveal イベント 123 から 対応していない 18.2 から
navigation.activation 123 から 147 から 26.2 から

仕様では、@view-transition を理解できないブラウザーは、この規則をエラーなしで読み飛ばすとされています。Safari 18.2 から 26.1 までは、@view-transition には対応していても navigation.activation がありません。上の「戻る」の例で window.navigation?.activation?.navigationType と ?. を付けているのは、navigation がないブラウザーで、undefined のプロパティを読んでエラーにならないようにするためです(?. は、左側が undefined や null のときにエラーにせず undefined を返します)。

​Posted by 86