View Transitions API 一覧⇔詳細
ネイティブの View Transitions API で一覧と詳細の切替を自動モーフ。共有サムネに view-transition-name を割り当て、SPA風の滑らかな画面遷移を実現します。
ライブデモ
使用例(お題: アイドルグループ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。