CSS の View Transitions API でページ遷移にアニメーションをつける

Web アプリでページを切り替えるとき、画面がパッと切り替わるだけだと唐突な印象を受けます。ネイティブアプリでは画面遷移にスライドやフェードが入るのが当たり前ですが、Web ではこれを実現するために複雑な JavaScript や FLIP アニメーションを組む必要がありました。View Transitions API はこの問題をブラウザレベルで解決する仕組みで、CSS だけで遷移アニメーションを定義できます。

仕組みの概要

View Transitions API は、遷移前の画面をスクリーンショットとしてキャプチャし、遷移後の新しい DOM と重ね合わせてアニメーションさせるという発想で動いています。開発者が個々の要素の位置計算やアニメーション制御をする必要はなく、ブラウザが自動的に古い状態と新しい状態の間を補間します。

遷移前の画面をキャプチャ(old snapshot)

DOM を更新する

遷移後の画面をキャプチャ(new snapshot)

2 つのスナップショット間をアニメーション

この処理はブラウザの合成レイヤー上で行われるため、DOM の再レイアウトを伴わず、パフォーマンス面でも有利です。

同一ドキュメント内の遷移(SPA)

SPA(Single Page Application)での遷移が最も基本的な使い方です。document.startViewTransition() を呼び出し、コールバック内で DOM を更新します。

document.startViewTransition(() => {
    // DOM を更新する処理
    updateContent(newPage);
});

これだけでデフォルトのクロスフェードアニメーションが適用されます。特別な CSS を書かなくても、古い画面がフェードアウトしながら新しい画面がフェードインする遷移が得られます。

デフォルトのアニメーションは ::view-transition-old::view-transition-new という疑似要素に対して定義されており、CSS でカスタマイズできます。

::view-transition-old(root) {
    animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
    animation: fade-in 0.3s ease-in;
}

@keyframes fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

root はページ全体を指すデフォルトの遷移名です。特定の要素だけに異なるアニメーションを割り当てたい場合は、view-transition-name を使って名前をつけます。

view-transition-name で要素を個別にアニメーションさせる

view-transition-name を指定した要素は、ページ全体の遷移とは独立して、その要素だけの遷移アニメーションが生成されます。たとえば記事一覧からカードをクリックして詳細ページに遷移するとき、カードの画像がそのまま詳細ページのヒーロー画像に変形するような演出が可能です。

/* 一覧ページのカード画像 */
.card-image {
    view-transition-name: hero-image;
}

/* 詳細ページのヒーロー画像 */
.detail-hero {
    view-transition-name: hero-image;
}

遷移前と遷移後で同じ view-transition-name を持つ要素がそれぞれ 1 つずつ存在すると、ブラウザはその要素間の位置・サイズ・不透明度を自動補間します。カード上の小さな画像がスムーズに拡大しながら詳細ページのヒーロー画像の位置に移動する、という動きが CSS だけで実現できます。

注意点として、同じページ内に同一の view-transition-name を持つ要素が複数存在すると遷移が失敗します。一覧ページでカードが複数並んでいる場合は、クリックされたカードだけに動的に名前を付与する必要があります。

function handleCardClick(card) {
    // クリックされたカードにだけ名前をつける
    card.querySelector('img').style.viewTransitionName = 'hero-image';

    document.startViewTransition(() => {
        updateToDetailPage();
    });
}

クロスドキュメント遷移(MPA)

SPA だけでなく、通常のページ遷移(MPA: Multi Page Application)でも View Transitions が使えます。こちらは JavaScript を書く必要がなく、CSS の @view-transition ルールで有効化します。

@view-transition {
    navigation: auto;
}

これを遷移元と遷移先の両方のページの CSS に記述するだけで、同一オリジン内のナビゲーションにクロスフェードが適用されます。navigation: auto は同一オリジンかつ通常のナビゲーション(リンククリックやフォーム送信)で自動的に遷移アニメーションを発火させる設定です。

MPA でも view-transition-name による要素間の補間は機能します。遷移元ページと遷移先ページで同じ名前を持つ要素があれば、ブラウザがその間をアニメーションさせます。

SPA(同一ドキュメント)

document.startViewTransition() を呼び出して DOM を更新する。JavaScript 必須だが、遷移タイミングを細かく制御できる。

MPA(クロスドキュメント)

@view-transition { navigation: auto } を CSS に書くだけ。JavaScript 不要だが、遷移の発火条件はブラウザ任せになる。

アニメーションのカスタマイズ

デフォルトのクロスフェード以外にも、スライドや拡大縮小など自由なアニメーションを定義できます。

/* スライドイン・アウト */
@keyframes slide-out-left {
    from { transform: translateX(0); }
    to { transform: translateX(-100%); }
}

@keyframes slide-in-right {
    from { transform: translateX(100%); }
    to { transform: translateX(0); }
}

::view-transition-old(root) {
    animation: slide-out-left 0.3s ease-in-out;
}

::view-transition-new(root) {
    animation: slide-in-right 0.3s ease-in-out;
}

疑似要素のツリー構造は以下のようになっています。

::view-transition

遷移全体のコンテナ。ページ最上位に生成される疑似要素で、すべての遷移グループを包含する。

::view-transition-group(name)

view-transition-name ごとに生成されるグループ。old と new のスナップショットを保持し、サイズや位置の補間アニメーションを担当する。

::view-transition-image-pair(name)

old と new の 2 つのスナップショットを保持するコンテナ。デフォルトでは isolation: auto が設定されており、新旧のブレンドを制御する。

::view-transition-old(name) / ::view-transition-new(name)

実際のスナップショット画像を表す疑似要素。old は遷移前のキャプチャ、new は遷移後のキャプチャに対応する。

この構造を理解しておくと、グループ単位での位置アニメーションと、スナップショット単位での不透明度アニメーションを分離して制御できます。

view-transition-class による一括指定

Chrome 125 以降では view-transition-class が使えます。複数の要素に同じアニメーションスタイルを適用したいとき、個別に view-transition-name を書いてからそれぞれの疑似要素にスタイルを当てるのは冗長です。view-transition-class を使えば、クラス単位でまとめてスタイリングできます。

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

/* すべての card-transition クラスに同じアニメーション */
::view-transition-group(*.card-transition) {
    animation-duration: 0.4s;
    animation-timing-function: ease-in-out;
}

アクセシビリティへの配慮

アニメーションが苦手なユーザーや、前庭機能障害を持つユーザーへの配慮として、prefers-reduced-motion メディアクエリで遷移を無効化またはシンプルにする対応が必要です。

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

View Transitions API 自体はこのメディアクエリを自動的には尊重しないため、開発者側で明示的に対応する必要があります。

ブラウザ対応状況

2024 年末時点の対応状況です。

機能ChromeFirefoxSafari
SPA 遷移114+対応なし18+
MPA 遷移126+対応なし18.2+
view-transition-class125+対応なし対応なし

Firefox は現時点で未対応ですが、開発の意向は示されています。未対応ブラウザでは document.startViewTransitionundefined になるため、フォールバック処理を入れておけば安全です。

function navigateTo(url, updateFn) {
    if (!document.startViewTransition) {
        updateFn();
        return;
    }
    document.startViewTransition(updateFn);
}

startViewTransition の存在チェックを挟むだけで、未対応環境では即座に DOM が更新され、対応環境ではアニメーション付きで遷移します。プログレッシブエンハンスメントの考え方にそのまま乗る API 設計になっています。