HOME / BLOG
BLOG

JavaScriptのView Transition APIの使い方【DOM更新をなめらかに切り替える】

デフォルト画像

絞り込みやテーマ切り替えでDOMを更新すると、内容が瞬時に入れ替わって変化を追いにくいことがあります。View Transition APIを使うと、更新前と更新後の表示をブラウザに捉えさせ、その間をCSSでアニメーションできます。

この記事では、記事カードをCSS/JavaScriptで絞り込むUIにdocument.startViewTransition()を追加します。未対応環境ではアニメーションなしで更新するフォールバックも入れます。

View Transition APIの流れ

  1. 現在の表示状態をブラウザが捉える
  2. startViewTransition()へ渡した更新処理を実行する
  3. 更新後の表示状態をブラウザが捉える
  4. 古い表示と新しい表示の間を疑似要素でアニメーションする

完成コード

HTML

<main class="app-shell">
  <img class="nines-logo" src="https://kjnine.com/wp-content/themes/kjnines_theme/images/svg2/nines_logo.svg" alt="NINES">
  <header>
    <p class="eyebrow">VIEW TRANSITION</p>
    <h1>記事カードの表示切り替え</h1>
    <div class="filter-buttons" aria-label="記事の絞り込み">
      <button type="button" data-filter="all" aria-pressed="true">すべて</button>
      <button type="button" data-filter="css" aria-pressed="false">CSS</button>
      <button type="button" data-filter="js" aria-pressed="false">JavaScript</button>
    </div>
  </header>

  <section id="articleGrid" class="article-grid" aria-live="polite"></section>
</main>

CSS

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  padding: 32px;
  background: #f1f4f8;
  color: #111;
  font-family: system-ui, sans-serif;
}

.app-shell {
  position: relative;
  width: min(100%, 920px);
  margin: 0 auto;
}

.nines-logo {
  position: absolute;
  top: 0;
  right: 0;
  width: 64px;
  opacity: 0.14;
}

.eyebrow {
  color: #0057ff;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.14em;
}

.filter-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 24px 0;
}

button {
  min-height: 42px;
  padding: 8px 14px;
  border: 1px solid #111;
  background: #fff;
  color: #111;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
}

button[aria-pressed="true"] {
  border-color: #0057ff;
  background: #0057ff;
  color: #fff;
}

.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  view-transition-name: article-grid;
}

.article-card {
  min-height: 180px;
  padding: 22px;
  border: 1px solid #d7deea;
  background: #fff;
}

.article-card span {
  color: #0057ff;
  font-size: 0.78rem;
  font-weight: 800;
}

::view-transition-old(article-grid) {
  animation: 180ms ease both fade-out;
}

::view-transition-new(article-grid) {
  animation: 260ms ease both fade-in;
}

@keyframes fade-out {
  to {
    opacity: 0;
    transform: translateY(-8px);
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

JavaScript

const articles = [
  { category: 'css', title: 'CSS Anchor Positioning入門' },
  { category: 'js', title: 'View Transition API入門' },
  { category: 'css', title: 'Scroll-driven Animations入門' },
  { category: 'js', title: 'Popover APIを制御する' },
];

const grid = document.querySelector('#articleGrid');
const buttons = document.querySelectorAll('[data-filter]');

function render(filter = 'all') {
  const filtered = filter === 'all'
    ? articles
    : articles.filter((article) => article.category === filter);

  grid.innerHTML = filtered.map((article) => `
    <article class="article-card">
      <span>${article.category.toUpperCase()}</span>
      <h2>${article.title}</h2>
      <p>標準Web APIでUIを作る実装メモです。</p>
    </article>
  `).join('');
}

function updateFilter(button) {
  buttons.forEach((item) => {
    item.setAttribute('aria-pressed', String(item === button));
  });
  render(button.dataset.filter);
}

buttons.forEach((button) => {
  button.addEventListener('click', () => {
    if (!document.startViewTransition) {
      updateFilter(button);
      return;
    }

    document.startViewTransition(() => updateFilter(button));
  });
});

render();

最小のJavaScript

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

コールバック内でDOMを更新します。APIが更新前後の状態を扱うため、アニメーション用にDOMを二重管理する必要がありません。サンプルでは絞り込み関数をそのままコールバックへ渡しています。

未対応環境のフォールバック

View Transition APIがなくても、絞り込み機能そのものは動くべきです。if (!document.startViewTransition)で確認し、未対応なら更新関数を直接呼びます。アニメーションを必須機能にしないことが重要です。

view-transition-nameを付ける

特定の要素を個別に扱う場合は、CSSでview-transition-nameを指定します。サンプルでは記事グリッド全体へarticle-gridという名前を付け、古いグリッドと新しいグリッドを別々にアニメーションしています。

疑似要素で動きを調整する

疑似要素役割
::view-transition-old(name)更新前のスナップショット
::view-transition-new(name)更新後のスナップショット
::view-transition-group(name)古い表示と新しい表示をまとめるグループ

更新前を上へ消し、更新後を下から表示するようにすると、フィルターが切り替わったことを短い動きで伝えられます。大きく移動させず、200〜300ms程度の軽い変化から調整するのがおすすめです。

使いやすい場面

  • 記事一覧や商品一覧の絞り込み
  • タブの内容切り替え
  • ライト/ダークテーマの切り替え
  • 詳細パネルの展開
  • SPA内の画面遷移

使わない方がよい場面

  • 更新自体が重く、アニメーション以前に待ち時間が長い
  • 動きが業務操作の速度を下げる
  • 変化が小さく、アニメーションする意味がない
  • 視覚効果が内容理解を邪魔する

動きを減らす設定へ対応する

prefers-reduced-motion: reduceではアニメーション時間をほぼゼロにします。UIの状態変更は残し、動きだけを減らします。派手さより、何が変わったか分かることを優先しましょう。

CodePen埋め込み用エリア

View Transition APIで記事カードを絞り込むUI
ここにCodePenのEmbedコードを貼り付けます。Pen作成後、このブロックと埋め込みタグを差し替えてください。

3つのパネルへコードを貼り、CSSとJavaScriptボタンを交互に押してください。次にview-transition-nameを外し、標準のルート遷移との違いを比べると理解しやすくなります。

あわせて読みたい

参考資料

まとめ

View Transition APIを使うと、DOM更新前後の変化を少ないコードでアニメーションできます。機能本体を先に作り、startViewTransition()は段階的な強化として追加します。フォールバックとprefers-reduced-motionを用意し、短く意味のある動きにしましょう。