パターン集

実戦で使う具体例。コピペして調整するための雛形として。

1. シンプルなクロスフェード

何もしなくてもデフォルトでこの挙動。期間だけ調整したい時はこれ。

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 350ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}

2. ダーク / ライト切替(クリック位置から円形展開)

Chrome の有名なデモ。クリック地点を起点に、新しいテーマが clip-path で 円形に広がっていく。

JS — クリック座標を CSS 変数で渡す
document.querySelector("#theme-toggle").addEventListener("click", (ev) => {
  const apply = () => document.documentElement.classList.toggle("dark")

  if (!document.startViewTransition) return apply()

  // クリック位置を CSS 変数に渡す
  const x = ev.clientX, y = ev.clientY
  document.documentElement.style.setProperty("--vt-x", `${x}px`)
  document.documentElement.style.setProperty("--vt-y", `${y}px`)
  document.documentElement.style.setProperty(
    "--vt-r",
    `${Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))}px`
  )

  document.startViewTransition(apply)
})
CSS — old は触らず、new を clip-path で広げる
::view-transition-old(root) {
  animation: none;       /* 古い見た目はそのまま固定 */
}
::view-transition-new(root) {
  animation: reveal 500ms cubic-bezier(.2, .8, .2, 1);
  transform-origin: var(--vt-x) var(--vt-y);
}

@keyframes reveal {
  from { clip-path: circle(0 at var(--vt-x) var(--vt-y)); }
  to   { clip-path: circle(var(--vt-r) at var(--vt-x) var(--vt-y)); }
}

ポイント:

3. リスト → 詳細(hero 遷移)

サムネイルがそのまま大きな画像に拡大される、よくあるネイティブ風の遷移。

一覧側
<ul class="cards">
  <li>
    <a href="#detail-3" data-id="3">
      <img src="thumbs/3.jpg" style="view-transition-name: card-3" />
    </a>
  </li>
</ul>
詳細側(同じ name)
<div id="detail-3" class="detail">
  <img src="hero/3.jpg" style="view-transition-name: card-3" />
</div>
JS — 切替時に startViewTransition でラップ
document.querySelectorAll(".cards a").forEach((a) => {
  a.addEventListener("click", (e) => {
    e.preventDefault()
    const id = a.dataset.id
    const apply = () => showDetail(id)

    if (!document.startViewTransition) return apply()
    document.startViewTransition(apply)
  })
})
CSS — 形だけ補間して image はクロスフェードしない
::view-transition-group(card-3) {
  animation-duration: 420ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}
/* old/new の同時表示によるブレを抑える */
::view-transition-old(card-3) { animation: none; opacity: 0; }
::view-transition-new(card-3) { animation: none; opacity: 1; }

4. ダイアログ open / close

<dialog> 要素の open/close を transition でアニメ化する。 中身に view-transition-name を付けるのがコツ。

<dialog id="modal" style="view-transition-name: modal-card">
  <h2>確認</h2>
  <p>実行しますか?</p>
</dialog>
<script>
  function openModal() {
    if (!document.startViewTransition) return modal.showModal()
    document.startViewTransition(() => modal.showModal())
  }
  function closeModal() {
    if (!document.startViewTransition) return modal.close()
    document.startViewTransition(() => modal.close())
  }
</script>
::view-transition-old(modal-card),
::view-transition-new(modal-card) {
  animation-duration: 220ms;
  animation-timing-function: cubic-bezier(.16, 1, .3, 1);
}
@keyframes modal-in  { from { opacity: 0; transform: scale(.96); } }
@keyframes modal-out { to   { opacity: 0; transform: scale(.98); } }
::view-transition-new(modal-card) { animation-name: modal-in; }
::view-transition-old(modal-card) { animation-name: modal-out; }

5. 並べ替え(FLIP の代替)

各アイテムに動的な view-transition-name を当てれば、 並び替え後の位置にスムーズに遷移する。FLIP テクニックを手で書かなくて良くなるのが大きい。

function reorder(nextOrder) {
  const apply = () => {
    list.replaceChildren(...nextOrder.map((id) => itemNode(id)))
  }
  if (!document.startViewTransition) return apply()
  document.startViewTransition(apply)
}

function itemNode(id) {
  const li = document.createElement("li")
  li.textContent = id
  li.style.viewTransitionName = `item-${id}` // 個別の name
  return li
}
::view-transition-group(*) {
  animation-duration: 350ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}

6. ページ送り(左右スライド・MPA)

マルチページサイトで、進む/戻るを左右スライドで表現する。

CSS(全ページ共通)
@view-transition { navigation: auto; }

@keyframes slide-out-left  { to   { transform: translateX(-30%); opacity: 0; } }
@keyframes slide-in-right  { from { transform: translateX( 30%); opacity: 0; } }
@keyframes slide-out-right { to   { transform: translateX( 30%); opacity: 0; } }
@keyframes slide-in-left   { from { transform: translateX(-30%); opacity: 0; } }

html:active-view-transition-type(forward) {
  &::view-transition-old(root) { animation: slide-out-left 320ms ease both; }
  &::view-transition-new(root) { animation: slide-in-right 320ms ease both; }
}
html:active-view-transition-type(back) {
  &::view-transition-old(root) { animation: slide-out-right 320ms ease both; }
  &::view-transition-new(root) { animation: slide-in-left  320ms ease both; }
}
遷移先で types を判定
window.addEventListener("pagereveal", (e) => {
  if (!e.viewTransition) return
  const a = navigation.activation
  const isBack =
    a.navigationType === "traverse" &&
    (a.from?.index ?? 0) > a.entry.index
  e.viewTransition.types.add(isBack ? "back" : "forward")
})

7. ヘッダだけは「居続ける」

ページ間でヘッダを滑らかに居座らせるパターン。両ページのヘッダに同じ name を当てる。

.site-header { view-transition-name: site-header; }

::view-transition-group(site-header) {
  /* 形は変わらない場合は短めに */
  animation-duration: 200ms;
}
::view-transition-old(site-header),
::view-transition-new(site-header) {
  animation: none;       /* old/new のクロスフェード自体を無効に */
}

old のクロスフェードを切ると、ヘッダ部分は本物の DOM がそのまま見え続けているかのように 振る舞う(実際は old=透明、new=不透明で連続表示されている)。

8. デフォルト挙動を全部上書きしてフェードを「消す」

ナビは即時切替がいい、でも個別要素にだけ transition を入れたい、というケース。

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}
/* 個別 name の transition だけ生かす */