0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

View Transitions APIはどこまで本番で使える?

0
Posted at

一覧から詳細へ遷移するとき、サムネイルがそのままヒーロー画像になる動きや、タブを切り替えると、下線がスッと移動するような動き。

こういう動きを自前で書こうとすると、FLIPを実装して getBoundingClientRect() で前後の位置を測り、差分を transform に変換して……という作業になります。ライブラリを入れても、DOM構造に制約が出ます。

View Transitions API は、これをブラウザに任せる仕組みです。DOMを更新する前後のスナップショットをブラウザが撮り、その差分をアニメーションしてくれます。

ただしこの機能は、2つに分かれていて、対応状況がまったく違います。ここを混同したまま記事のコードをコピーすると、「書いた通りなのに動かない」が起きます。

2つの View Transitions

同一ドキュメント(Level 1)

document.startViewTransition() で呼び出すもの。SPAの画面切り替え、タブ、リストのフィルタリングや並べ替えなど、1つのページの中での状態変化に使います。

Baseline newly available(2025年10月〜)。Chrome 111、Safari 18、Firefox 144 で対応済みです。

本番投入していい問題ありません。

ページ間(Level 2 / クロスドキュメント)

CSSの @view-transition で宣言するもの。MPAでのページからページへの遷移に使います。

Limited availability。Chrome 126、Safari 18.2 で対応していますが、Firefox が未対応です(Interop 2026 の対象項目)。

MDNも明確に「一部の広く使われているブラウザで動作しないため、Baselineではない」と書いています。

つまり、プログレッシブエンハンスメントとして載せるのはよいが、体験の前提にはできない段階です。

以降、両方を扱いますが、この前提の違いを頭に置いて読んでください。

基本の仕組み

document.startViewTransition(() => {
  // ここでDOMを更新する
  renderNewState();
});

ブラウザがやっていることは4ステップです。

  1. 更新前の画面のスナップショットを撮る
  2. コールバックを実行してDOMを更新する
  3. 更新後のスナップショットを撮る
  4. 2枚を重ねてアニメーションする

既定では全画面のクロスフェードになります。ここまでコード3行。React でも Vue でも、DOMを更新する処理をこのコールバックで包むだけです。

要素を「移動」させる

本領はここからです。view-transition-name を付けた要素は、他とは独立して追跡され、位置とサイズが補間されます。

.hero-image {
  view-transition-name: hero;
}

遷移前と遷移後で同じ名前を持つ要素があれば、ブラウザは「これは同じものが移動した」と解釈して、その間を繋ぎます。一覧のサムネイルが詳細画面のヒーロー画像に広がる、あの動きです。

FLIPを手で書いていた部分が、CSSのプロパティ1つになります。

最大の落とし穴:名前は同時に1つだけ

同じ view-transition-name を持つ要素が同時に2つ以上存在すると、トランジション全体が失敗します。 しかもエラーにならず、静かにスキップされます。

リストの各アイテムに同じ名前を付けると、これを踏みます。

/* ❌ 一覧の全カードが同じ名前を持ってしまう */
.card__image { view-transition-name: hero; }

正しくは、クリックされた要素にだけ動的に付けて、終わったら外すという書き方になります。

function openDetail(item) {
  item.style.viewTransitionName = 'hero';

  const transition = document.startViewTransition(async () => {
    await navigateToDetail(item.dataset.id);
  });

  transition.finished.finally(() => {
    item.style.viewTransitionName = '';  // 後始末を忘れない
  });
}

finished の後始末を忘れると、次の遷移で名前が重複して動かなくなります。デバッグしづらいので、最初から書いておいてください。

擬似要素ツリーを理解する

アニメーションをカスタマイズするには、ブラウザが生成する擬似要素の構造を知る必要があります。

::view-transition
└─ ::view-transition-group(name)
   └─ ::view-transition-image-pair(name)
      ├─ ::view-transition-old(name)   古い状態のスナップショット
      └─ ::view-transition-new(name)   新しい状態

役割分担はこうです。

  • group … 位置とサイズのアニメーションを担当する。要素が「移動する」動きはここ
  • old / new … 中身の画像。既定ではクロスフェードする

なので「移動の速さを変えたい」なら group、「フェードの仕方を変えたい」なら old / new を触ります。

::view-transition-old(root) {
  animation: 200ms ease-out both fade-out;
}

::view-transition-new(root) {
  animation: 300ms ease-in both slide-up;
}

* を使うと全部をまとめて狙えます。

::view-transition-group(*) {
  animation-duration: 250ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

root は、名前を付けていない部分すべてを指す既定のグループ名です。

画像が歪む問題

遷移前と遷移後でアスペクト比が違うと、既定では引き伸ばされて歪みます。

サムネイル(正方形)からヒーロー画像(横長)への遷移は、まさにこのケースです。

::view-transition-old(hero),
::view-transition-new(hero) {
  object-fit: cover;
  height: 100%;
}

object-fit: cover を当てておけば、比率を保ったまま切り抜かれます。

「進む」と「戻る」を出し分ける

同じ遷移でも、進むときは左へ、戻るときは右へ動かしたい。これを types で表現できます。

document.startViewTransition({
  update: () => render(nextPage),
  types: ['forward'],
});

CSS側で受けます。

html:active-view-transition-type(forward) {
  &::view-transition-old(root) { animation: slide-out-left 300ms both; }
  &::view-transition-new(root) { animation: slide-in-right 300ms both; }
}

html:active-view-transition-type(back) {
  &::view-transition-old(root) { animation: slide-out-right 300ms both; }
  &::view-transition-new(root) { animation: slide-in-left 300ms both; }
}

引数なしの :active-view-transition もあります。こちらは トランジション実行中だけルート要素にマッチする擬似クラスで、Baseline newly available(2026年1月〜)です。

アニメーション中に浮いて見えてしまう要素を隠すのに便利です。

:root:active-view-transition .floating-action-button {
  visibility: hidden;
}

なお、types と :active-view-transition-type() は比較的新しく、対応状況に差が残っています。これに依存した見た目は「あると嬉しい」程度に留めるか、@supports で分岐させてください。

ページ間の遷移(クロスドキュメント)

MPAでのページ遷移は、CSSだけで書けます。

@view-transition {
  navigation: auto;
}

これだけで、ページ遷移がクロスフェードになります。JavaScript はゼロ行です。

驚くほど簡単ですが、制約がかなり多いので、そこを把握してから入れてください。

発動条件

  • 遷移元と遷移先の両方が opt-in していること。片方だけでは発動しません
  • 同一オリジンであること。クロスオリジンのリンクは対象外です
  • ユーザー起点のナビゲーションであること。location.href = ... のようなプログラム的な遷移やリダイレクトはスキップされます
  • 遷移先が4秒以内にレンダリングされること。超えるとタイムアウトして、静かに失敗します

最後の2つが実務では重いと思っています。

特に4秒のタイムアウトは、サーバーの応答が遅かったり、レンダリングブロックするリソースが多かったりすると普通に踏みます。しかも失敗してもエラーは出ず、ただ普通の遷移になるだけなので、気づきにくい。

裏を返せば、View Transitions を入れるなら、その前に表示速度を改善するほうが先ということです。遷移が速いページでしか、この機能は成立しません。

pageswap / pagereveal

遷移の前後でJavaScriptからフックできます。

// 出ていくページ側
addEventListener('pageswap', (e) => {
  if (!e.viewTransition) return;  // ガードは必須

  const url = new URL(e.activation.entry.url);
  // 遷移先のURLを見て、view-transition-name を付け替えるなど
});

// 入ってくるページ側
addEventListener('pagereveal', (e) => {
  if (!e.viewTransition) return;
});

e.viewTransition は null になり得ます。 非対応ブラウザ、opt-inしていない、発動条件を満たさない、といったケースです。

そしてイベント自体は発火するので、ガードなしで e.viewTransition.ready などと書くとエラーになります。必ず最初にチェックしてください。

prefers-reduced-motion は自分で扱う

ブラウザは自動でスキップしてくれません。 ここは必ず対応してください。

CSSで一括して止められます。

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

JavaScript側で分岐する場合はこうです。

function update() { /* DOM更新 */ }

const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

if (reduce || !document.startViewTransition) {
  update();
} else {
  document.startViewTransition(update);
}

画面全体がスライドしたりズームしたりする動きは、前庭障害のある人にとって強い負担になります。View Transitions は画面全体が動く機能なので、影響は大きいほうです。省略しないでください。

なお、この分岐は非対応ブラウザのフォールバックも兼ねています。document.startViewTransition の存在チェックだけで、古い環境でも普通に動きます。

アクセシビリティのその他の注意

トランジション中は、古い画面のスナップショットと新しい画面が視覚的に重なっています。しかしアクセシビリティツリーから見えているのは、更新後のDOMだけです。

つまり、見た目とスクリーンリーダーの認識にズレが生じます。これ自体は仕様どおりですが、次の2点は自分で面倒を見る必要があります。

  • フォーカスの管理。ページ遷移後に、見出しなど適切な場所へフォーカスを移す処理は自分で書く必要があります
  • ライブリージョンの読み上げ。アニメーション中に更新が走ると、読み上げのタイミングが噛み合わないことがあります

見た目が滑らかになっても、支援技術に伝わる内容は変わりません。CSSやアニメーションで解決できる領域ではない、という点は押さえておいてください。

導入の判断

まとめると、2026年9月時点ではこうなります。

同一ドキュメント(startViewTransition())は、本番投入して問題ありません。

Baseline newly available で3エンジンすべてが対応済み。存在チェック1行でフォールバックも書けます。SPAのルーティング、タブ、フィルタリング、並べ替え、モーダルの開閉。このあたりは今すぐ置き換えていいと思います。

ページ間(@view-transition)は、Firefox未対応です。

非対応ブラウザは @view-transition を無視して普通の遷移になるだけなので、載せること自体にリスクはありません。ただし「この遷移アニメーションがあることを前提にしたUI設計」はしないでください。半分のユーザーには見えない可能性があります。

まとめ

  • View Transitions は同一ドキュメントとページ間で対応状況がまったく違う。記事を読むときはどちらの話か確認する
  • 同一ドキュメントは Baseline newly available(2025年10月〜)で本番投入可
  • ページ間は Firefox 未対応。プログレッシブエンハンスメントとしてなら載せてよい
  • view-transition-name の重複が最大の事故要因。クリックされた要素に動的に付け、finished で後始末する
  • 位置・サイズは ::view-transition-group、中身のフェードは old / new
  • 画像は object-fit: cover を当てないと歪む
  • クロスドキュメントは同一オリジン・ユーザー起点・4秒以内のレンダリングが条件。遅いページでは成立しない
  • prefers-reduced-motion の対応は必須。ブラウザはスキップしてくれない

「アニメーションのためにライブラリを入れる」時代は、そろそろ終わりつつあります。少なくとも同一ドキュメント側は、もう標準で十分です。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?