View Transitions API 一覧⇔詳細

ネイティブの View Transitions API で一覧と詳細の切替を自動モーフ。共有サムネに view-transition-name を割り当て、SPA風の滑らかな画面遷移を実現します。

#css#javascript#view-transitions#animation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura: メンバー一覧⇔プロフィール詳細を View Transitions で滑らかにモーフ -->
<div class="sk-app">
  <header class="sk-head">
    <span class="sk-brand">🌸 Sakura</span>
    <span class="sk-head-sub">MEMBER</span>
  </header>

  <!-- 一覧ビュー -->
  <section class="sk-grid" data-view="grid" aria-label="メンバー一覧">
    <button class="sk-card" data-id="1" style="--c1:#ffd1e0;--c2:#ff8fb3">
      <span class="sk-thumb" style="view-transition-name:sk-img-1"></span>
      <span class="sk-card-body">
        <span class="sk-card-name">星野 ひなた</span>
        <span class="sk-card-color">桜色担当</span>
      </span>
    </button>
    <button class="sk-card" data-id="2" style="--c1:#ffe3ef;--c2:#ffa9c9">
      <span class="sk-thumb" style="view-transition-name:sk-img-2"></span>
      <span class="sk-card-body">
        <span class="sk-card-name">月見 さくら</span>
        <span class="sk-card-color">白桃担当</span>
      </span>
    </button>
    <button class="sk-card" data-id="3" style="--c1:#f7d9ff;--c2:#d59bff">
      <span class="sk-thumb" style="view-transition-name:sk-img-3"></span>
      <span class="sk-card-body">
        <span class="sk-card-name">花咲 ことね</span>
        <span class="sk-card-color">藤色担当</span>
      </span>
    </button>
  </section>

  <!-- 詳細ビュー -->
  <section class="sk-detail" data-view="detail" hidden aria-label="メンバー詳細">
    <span class="sk-detail-img"></span>
    <div class="sk-detail-body">
      <span class="sk-detail-color">桜色担当</span>
      <h2 class="sk-detail-name">星野 ひなた</h2>
      <p class="sk-detail-text">グループのセンターを務める。明るい笑顔とよく通る歌声がチャームポイント。好きな食べ物はいちご大福。</p>
      <div class="sk-detail-tags">
        <span>#リーダー</span><span>#ボーカル</span><span>#3期生</span>
      </div>
      <button class="sk-back">← メンバー一覧へ</button>
    </div>
  </section>

  <p class="sk-fallback" data-fallback hidden>※ お使いのブラウザは View Transitions 未対応のため通常表示です</p>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 400px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  color: #5a4150;
  background:
    radial-gradient(700px 380px at 80% -10%, #ffe7f0 0%, transparent 60%),
    #fff6fa;
}

.sk-app {
  width: min(560px, 94vw);
  background: #fff;
  border-radius: 20px;
  padding: 18px;
  box-shadow: 0 18px 44px rgba(255, 150, 185, .22);
}

.sk-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 2px 4px 16px;
}
.sk-brand { font-size: 19px; font-weight: 800; color: #ff6fa3; letter-spacing: .03em; }
.sk-head-sub { font-size: 11px; letter-spacing: .35em; color: #c79ab0; font-weight: 700; }

/* 一覧グリッド */
.sk-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}

.sk-card {
  border: none;
  padding: 0;
  border-radius: 16px;
  background: #fff;
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  box-shadow: 0 6px 18px rgba(255, 150, 185, .2);
  transition: transform .22s ease, box-shadow .22s ease;
}
.sk-card:hover { transform: translateY(-4px); box-shadow: 0 14px 28px rgba(255, 130, 175, .3); }
.sk-card:focus-visible { outline: 2px solid #ff8fb3; outline-offset: 3px; }

.sk-thumb {
  display: block;
  aspect-ratio: 3 / 4;
  background:
    radial-gradient(circle at 50% 38%, rgba(255,255,255,.55) 0 14%, transparent 16%),
    linear-gradient(150deg, var(--c1), var(--c2));
}

.sk-card-body { display: block; padding: 9px 10px; }
.sk-card-name { display: block; font-size: 13px; font-weight: 700; color: #4a3540; }
.sk-card-color { display: block; margin-top: 2px; font-size: 10px; color: #c79ab0; }

/* 詳細ビュー */
.sk-detail {
  display: grid;
  grid-template-columns: 150px 1fr;
  gap: 20px;
  align-items: center;
}
.sk-detail[hidden] { display: none; }
.sk-grid[hidden] { display: none; }

.sk-detail-img {
  aspect-ratio: 3 / 4;
  border-radius: 16px;
  background:
    radial-gradient(circle at 50% 38%, rgba(255,255,255,.55) 0 14%, transparent 16%),
    linear-gradient(150deg, var(--c1, #ffd1e0), var(--c2, #ff8fb3));
  view-transition-name: sk-hero;
  box-shadow: 0 10px 26px rgba(255, 130, 175, .3);
}

.sk-detail-color { font-size: 11px; letter-spacing: .2em; color: #ff8fb3; font-weight: 700; }
.sk-detail-name { margin: 6px 0 10px; font-size: 24px; color: #4a3540; }
.sk-detail-text { margin: 0 0 14px; font-size: 13px; line-height: 1.8; color: #7d6471; }

.sk-detail-tags { display: flex; gap: 7px; margin-bottom: 18px; flex-wrap: wrap; }
.sk-detail-tags span {
  font-size: 11px;
  color: #ff6fa3;
  background: #ffeef4;
  padding: 4px 10px;
  border-radius: 999px;
}

.sk-back {
  border: 1px solid #ffc6da;
  background: #fff;
  color: #ff6fa3;
  padding: 9px 18px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  transition: background .2s ease;
}
.sk-back:hover { background: #ffeef4; }

.sk-fallback { margin: 12px 4px 0; font-size: 11px; color: #c79ab0; }

/* View Transitions 演出 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .44s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .sk-card { transition: none; }
}
JavaScript
// Sakura メンバー一覧⇔詳細を startViewTransition で共有サムネをモーフ
(() => {
  const grid = document.querySelector('.sk-grid');
  const detail = document.querySelector('.sk-detail');
  const backBtn = document.querySelector('.sk-back');
  const detailImg = document.querySelector('.sk-detail-img');
  const detailName = document.querySelector('.sk-detail-name');
  const detailColor = document.querySelector('.sk-detail-color');
  const detailText = document.querySelector('.sk-detail-text');
  const fallback = document.querySelector('[data-fallback]');
  if (!grid || !detail || !backBtn) return; // null安全

  // 各メンバーの詳細プロフィール
  const profiles = {
    '1': 'グループのセンターを務める。明るい笑顔とよく通る歌声がチャームポイント。好きな食べ物はいちご大福。',
    '2': '透明感のある歌声でバラードを支える癒し系。読書とカフェ巡りが趣味で、作詞にも挑戦中。',
    '3': 'キレのあるダンスが武器のパフォーマー。マイペースな天然キャラでメンバーからの信頼も厚い。'
  };

  // API未対応ならフォールバック表示
  const supported = typeof document.startViewTransition === 'function';
  if (!supported && fallback) fallback.hidden = false;

  // DOM更新を遷移でラップ
  const run = (update) => {
    if (supported) document.startViewTransition(update);
    else update();
  };

  // カード→詳細へ
  grid.addEventListener('click', (e) => {
    const card = e.target.closest('.sk-card');
    if (!card) return;
    const thumb = card.querySelector('.sk-thumb');
    const name = card.querySelector('.sk-card-name')?.textContent ?? '';
    const color = card.querySelector('.sk-card-color')?.textContent ?? '';
    const id = card.dataset.id ?? '';

    run(() => {
      // 共有名を一旦外し、対象サムネだけ詳細へ引き継ぐ
      grid.querySelectorAll('.sk-thumb').forEach((t) => { t.style.viewTransitionName = ''; });
      if (thumb) thumb.style.viewTransitionName = 'sk-hero';

      detailImg.style.setProperty('--c1', card.style.getPropertyValue('--c1'));
      detailImg.style.setProperty('--c2', card.style.getPropertyValue('--c2'));
      detailName.textContent = name;
      detailColor.textContent = color;
      if (detailText) detailText.textContent = profiles[id] ?? detailText.textContent;

      grid.hidden = true;
      detail.hidden = false;
    });
  });

  // 詳細→一覧へ戻る
  backBtn.addEventListener('click', () => {
    run(() => {
      detail.hidden = true;
      grid.hidden = false;
      // 共有名を元のカードへ振り直し(連続遷移でも破綻しない)
      grid.querySelectorAll('.sk-thumb').forEach((t, i) => {
        t.style.viewTransitionName = `sk-img-${i + 1}`;
      });
    });
  });
})();

コード

HTML
<!-- View Transitions API: ギャラリー⇔詳細をネイティブ遷移で滑らかに切替 -->
<div class="vt-stage">
  <!-- 一覧ビュー -->
  <section class="vt-grid" data-view="grid" aria-label="作品一覧">
    <button class="vt-card" data-id="1" style="--c1:#7c5cff;--c2:#22d3ee">
      <span class="vt-thumb" style="view-transition-name:vt-img-1"></span>
      <span class="vt-card-title">Aurora</span>
    </button>
    <button class="vt-card" data-id="2" style="--c1:#ff7eb3;--c2:#ff758c">
      <span class="vt-thumb" style="view-transition-name:vt-img-2"></span>
      <span class="vt-card-title">Sunset</span>
    </button>
    <button class="vt-card" data-id="3" style="--c1:#43e97b;--c2:#38f9d7">
      <span class="vt-thumb" style="view-transition-name:vt-img-3"></span>
      <span class="vt-card-title">Mint</span>
    </button>
  </section>

  <!-- 詳細ビュー -->
  <section class="vt-detail" data-view="detail" hidden aria-label="作品詳細">
    <span class="vt-detail-img"></span>
    <div class="vt-detail-body">
      <h2 class="vt-detail-title">Aurora</h2>
      <p class="vt-detail-text">View Transitions API はDOM更新の前後を自動でクロスフェード・モーフ。共有要素には view-transition-name を割り当てるだけ。</p>
      <button class="vt-back">← 一覧へ戻る</button>
    </div>
  </section>

  <p class="vt-note" data-fallback hidden>※ お使いのブラウザは View Transitions 未対応のためフォールバック表示です</p>
</div>
CSS
* { box-sizing: border-box; }

:root {
  --bg: #0f1020;
  --panel: #1a1b2e;
  --text: #eef0ff;
  --muted: #9aa0c4;
  --radius: 16px;
}

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
  background:
    radial-gradient(900px 400px at 80% -10%, #2a2350 0%, transparent 60%),
    radial-gradient(700px 360px at 0% 120%, #143a4a 0%, transparent 55%),
    var(--bg);
}

.vt-stage {
  width: min(680px, 92vw);
  padding: 22px;
}

/* 一覧グリッド */
.vt-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}

.vt-card {
  position: relative;
  border: none;
  padding: 0;
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0,0,0,.35);
  transition: transform .25s ease, box-shadow .25s ease;
}
.vt-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 36px rgba(0,0,0,.5);
}
.vt-card:focus-visible { outline: 2px solid #7c5cff; outline-offset: 3px; }

.vt-thumb {
  display: block;
  aspect-ratio: 4 / 3;
  background: linear-gradient(135deg, var(--c1), var(--c2));
}

.vt-card-title {
  display: block;
  padding: 10px 12px;
  font-size: 14px;
  font-weight: 600;
  text-align: left;
  color: var(--text);
}

/* 詳細ビュー */
.vt-detail {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 20px;
  align-items: center;
  background: var(--panel);
  border-radius: var(--radius);
  padding: 22px;
  box-shadow: 0 16px 40px rgba(0,0,0,.45);
}
.vt-detail[hidden] { display: none; }
.vt-grid[hidden] { display: none; }

.vt-detail-img {
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  background: linear-gradient(135deg, var(--c1, #7c5cff), var(--c2, #22d3ee));
  view-transition-name: vt-hero;
}

.vt-detail-title { margin: 0 0 8px; font-size: 24px; }
.vt-detail-text { margin: 0 0 18px; color: var(--muted); font-size: 14px; line-height: 1.7; }

.vt-back {
  border: 1px solid #3a3c5e;
  background: transparent;
  color: var(--text);
  padding: 9px 16px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 13px;
  transition: background .2s ease, border-color .2s ease;
}
.vt-back:hover { background: #26284a; border-color: #5a5c8e; }

.vt-note {
  margin: 14px 2px 0;
  font-size: 12px;
  color: var(--muted);
}

/* View Transitions の演出(共有要素のモーフ+クロスフェード) */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .42s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

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

@media (max-width: 520px) {
  .vt-detail { grid-template-columns: 1fr; }
  .vt-detail-img { max-width: 200px; }
}
JavaScript
// View Transitions API デモ: 一覧⇔詳細の切替を startViewTransition で滑らかに
(() => {
  const grid = document.querySelector('.vt-grid');
  const detail = document.querySelector('.vt-detail');
  const backBtn = document.querySelector('.vt-back');
  const detailImg = document.querySelector('.vt-detail-img');
  const detailTitle = document.querySelector('.vt-detail-title');
  const fallbackNote = document.querySelector('[data-fallback]');
  if (!grid || !detail || !backBtn) return; // null安全

  // API未対応ならフォールバック(瞬時切替+注記)
  const supported = typeof document.startViewTransition === 'function';
  if (!supported && fallbackNote) fallbackNote.hidden = false;

  // DOM更新を関数化し、対応ブラウザでは遷移でラップ
  const run = (update) => {
    if (supported) document.startViewTransition(update);
    else update();
  };

  // カード→詳細へ
  grid.addEventListener('click', (e) => {
    const card = e.target.closest('.vt-card');
    if (!card) return;
    const thumb = card.querySelector('.vt-thumb');
    const title = card.querySelector('.vt-card-title')?.textContent ?? '';
    // クリックされたサムネを共有要素として詳細画像へ引き継ぐ
    const sharedName = thumb ? getComputedStyle(thumb).viewTransitionName : '';

    run(() => {
      // 一旦すべてのthumbの共有名を外し、対象だけ付け替える
      grid.querySelectorAll('.vt-thumb').forEach((t) => { t.style.viewTransitionName = ''; });
      if (thumb) thumb.style.viewTransitionName = 'vt-hero';

      const c1 = card.style.getPropertyValue('--c1');
      const c2 = card.style.getPropertyValue('--c2');
      detailImg.style.setProperty('--c1', c1);
      detailImg.style.setProperty('--c2', c2);
      detailTitle.textContent = title;

      grid.hidden = true;
      detail.hidden = false;
    });
  });

  // 詳細→一覧へ戻る
  backBtn.addEventListener('click', () => {
    run(() => {
      detail.hidden = true;
      grid.hidden = false;
      // 共有名を元のカードへ戻す(連続遷移でも破綻しないよう毎回振り直し)
      grid.querySelectorAll('.vt-thumb').forEach((t, i) => {
        t.style.viewTransitionName = `vt-img-${i + 1}`;
      });
    });
  });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「View Transitions API 一覧⇔詳細」の効果を追加してください。

# 追加してほしい効果
View Transitions API 一覧⇔詳細(ページ遷移 / View Transitions)
ネイティブの View Transitions API で一覧と詳細の切替を自動モーフ。共有サムネに view-transition-name を割り当て、SPA風の滑らかな画面遷移を実現します。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- View Transitions API: ギャラリー⇔詳細をネイティブ遷移で滑らかに切替 -->
<div class="vt-stage">
  <!-- 一覧ビュー -->
  <section class="vt-grid" data-view="grid" aria-label="作品一覧">
    <button class="vt-card" data-id="1" style="--c1:#7c5cff;--c2:#22d3ee">
      <span class="vt-thumb" style="view-transition-name:vt-img-1"></span>
      <span class="vt-card-title">Aurora</span>
    </button>
    <button class="vt-card" data-id="2" style="--c1:#ff7eb3;--c2:#ff758c">
      <span class="vt-thumb" style="view-transition-name:vt-img-2"></span>
      <span class="vt-card-title">Sunset</span>
    </button>
    <button class="vt-card" data-id="3" style="--c1:#43e97b;--c2:#38f9d7">
      <span class="vt-thumb" style="view-transition-name:vt-img-3"></span>
      <span class="vt-card-title">Mint</span>
    </button>
  </section>

  <!-- 詳細ビュー -->
  <section class="vt-detail" data-view="detail" hidden aria-label="作品詳細">
    <span class="vt-detail-img"></span>
    <div class="vt-detail-body">
      <h2 class="vt-detail-title">Aurora</h2>
      <p class="vt-detail-text">View Transitions API はDOM更新の前後を自動でクロスフェード・モーフ。共有要素には view-transition-name を割り当てるだけ。</p>
      <button class="vt-back">← 一覧へ戻る</button>
    </div>
  </section>

  <p class="vt-note" data-fallback hidden>※ お使いのブラウザは View Transitions 未対応のためフォールバック表示です</p>
</div>

【CSS】
* { box-sizing: border-box; }

:root {
  --bg: #0f1020;
  --panel: #1a1b2e;
  --text: #eef0ff;
  --muted: #9aa0c4;
  --radius: 16px;
}

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
  background:
    radial-gradient(900px 400px at 80% -10%, #2a2350 0%, transparent 60%),
    radial-gradient(700px 360px at 0% 120%, #143a4a 0%, transparent 55%),
    var(--bg);
}

.vt-stage {
  width: min(680px, 92vw);
  padding: 22px;
}

/* 一覧グリッド */
.vt-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}

.vt-card {
  position: relative;
  border: none;
  padding: 0;
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0,0,0,.35);
  transition: transform .25s ease, box-shadow .25s ease;
}
.vt-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 36px rgba(0,0,0,.5);
}
.vt-card:focus-visible { outline: 2px solid #7c5cff; outline-offset: 3px; }

.vt-thumb {
  display: block;
  aspect-ratio: 4 / 3;
  background: linear-gradient(135deg, var(--c1), var(--c2));
}

.vt-card-title {
  display: block;
  padding: 10px 12px;
  font-size: 14px;
  font-weight: 600;
  text-align: left;
  color: var(--text);
}

/* 詳細ビュー */
.vt-detail {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 20px;
  align-items: center;
  background: var(--panel);
  border-radius: var(--radius);
  padding: 22px;
  box-shadow: 0 16px 40px rgba(0,0,0,.45);
}
.vt-detail[hidden] { display: none; }
.vt-grid[hidden] { display: none; }

.vt-detail-img {
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  background: linear-gradient(135deg, var(--c1, #7c5cff), var(--c2, #22d3ee));
  view-transition-name: vt-hero;
}

.vt-detail-title { margin: 0 0 8px; font-size: 24px; }
.vt-detail-text { margin: 0 0 18px; color: var(--muted); font-size: 14px; line-height: 1.7; }

.vt-back {
  border: 1px solid #3a3c5e;
  background: transparent;
  color: var(--text);
  padding: 9px 16px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 13px;
  transition: background .2s ease, border-color .2s ease;
}
.vt-back:hover { background: #26284a; border-color: #5a5c8e; }

.vt-note {
  margin: 14px 2px 0;
  font-size: 12px;
  color: var(--muted);
}

/* View Transitions の演出(共有要素のモーフ+クロスフェード) */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .42s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

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

@media (max-width: 520px) {
  .vt-detail { grid-template-columns: 1fr; }
  .vt-detail-img { max-width: 200px; }
}

【JavaScript】
// View Transitions API デモ: 一覧⇔詳細の切替を startViewTransition で滑らかに
(() => {
  const grid = document.querySelector('.vt-grid');
  const detail = document.querySelector('.vt-detail');
  const backBtn = document.querySelector('.vt-back');
  const detailImg = document.querySelector('.vt-detail-img');
  const detailTitle = document.querySelector('.vt-detail-title');
  const fallbackNote = document.querySelector('[data-fallback]');
  if (!grid || !detail || !backBtn) return; // null安全

  // API未対応ならフォールバック(瞬時切替+注記)
  const supported = typeof document.startViewTransition === 'function';
  if (!supported && fallbackNote) fallbackNote.hidden = false;

  // DOM更新を関数化し、対応ブラウザでは遷移でラップ
  const run = (update) => {
    if (supported) document.startViewTransition(update);
    else update();
  };

  // カード→詳細へ
  grid.addEventListener('click', (e) => {
    const card = e.target.closest('.vt-card');
    if (!card) return;
    const thumb = card.querySelector('.vt-thumb');
    const title = card.querySelector('.vt-card-title')?.textContent ?? '';
    // クリックされたサムネを共有要素として詳細画像へ引き継ぐ
    const sharedName = thumb ? getComputedStyle(thumb).viewTransitionName : '';

    run(() => {
      // 一旦すべてのthumbの共有名を外し、対象だけ付け替える
      grid.querySelectorAll('.vt-thumb').forEach((t) => { t.style.viewTransitionName = ''; });
      if (thumb) thumb.style.viewTransitionName = 'vt-hero';

      const c1 = card.style.getPropertyValue('--c1');
      const c2 = card.style.getPropertyValue('--c2');
      detailImg.style.setProperty('--c1', c1);
      detailImg.style.setProperty('--c2', c2);
      detailTitle.textContent = title;

      grid.hidden = true;
      detail.hidden = false;
    });
  });

  // 詳細→一覧へ戻る
  backBtn.addEventListener('click', () => {
    run(() => {
      detail.hidden = true;
      grid.hidden = false;
      // 共有名を元のカードへ戻す(連続遷移でも破綻しないよう毎回振り直し)
      grid.querySelectorAll('.vt-thumb').forEach((t, i) => {
        t.style.viewTransitionName = `vt-img-${i + 1}`;
      });
    });
  });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。