カーテン遷移スライダー
上下のパネルが中央で閉じ切る瞬間に裏でコンテンツを差し替えるカーテン演出。シーン切替やページ遷移のつなぎに使える劇的なトランジションです。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura: ライブMVのシーンをカーテン遷移で劇的に切替 -->
<div class="sc-app">
<div class="sc-head">
<span class="sc-brand">🌸 Sakura</span>
<span class="sc-sub">LIVE TOUR 2026 「春爛漫」</span>
</div>
<div class="sc-screen">
<!-- 表示中のシーン -->
<article class="sc-slide is-active" data-slide="0">
<span class="sc-kicker">OPENING ACT</span>
<h2 class="sc-title">満開のステージ</h2>
<p class="sc-text">桜吹雪が舞う開幕。9人が一斉にセンターステージへ駆け上がる。</p>
</article>
<article class="sc-slide" data-slide="1">
<span class="sc-kicker">M-05 BALLADE</span>
<h2 class="sc-title">夜空のメロディ</h2>
<p class="sc-text">照明が落ち、ピアノの旋律にのせて届ける珠玉のバラード。</p>
</article>
<article class="sc-slide" data-slide="2">
<span class="sc-kicker">FINALE</span>
<h2 class="sc-title">また会う日まで</h2>
<p class="sc-text">銀テープが舞い散るクライマックス。会場全体が一体となる大合唱。</p>
</article>
<!-- カーテン(上下2枚) -->
<span class="sc-panel sc-panel--top" aria-hidden="true"></span>
<span class="sc-panel sc-panel--bottom" aria-hidden="true"></span>
</div>
<div class="sc-controls">
<button class="sc-nav" data-dir="-1" aria-label="前のシーン">‹</button>
<div class="sc-dots" role="tablist">
<button class="sc-dot is-on" data-go="0" aria-label="シーン1"></button>
<button class="sc-dot" data-go="1" aria-label="シーン2"></button>
<button class="sc-dot" data-go="2" aria-label="シーン3"></button>
</div>
<button class="sc-nav" data-dir="1" aria-label="次のシーン">›</button>
</div>
</div>
CSS
* { box-sizing: border-box; }
:root {
--dur: 560ms;
--ease: cubic-bezier(.76, 0, .24, 1);
--curtain: #fff;
}
body {
margin: 0;
min-height: 400px;
display: grid;
place-items: center;
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
color: #fff;
background:
radial-gradient(640px 340px at 80% -10%, #ffe7f0 0%, transparent 60%),
#fff6fa;
}
.sc-app { width: min(580px, 94vw); }
.sc-head { display: flex; align-items: baseline; gap: 12px; margin: 0 4px 12px; }
.sc-brand { font-size: 18px; font-weight: 800; color: #ff6fa3; }
.sc-sub { font-size: 12px; color: #c79ab0; font-weight: 600; }
.sc-screen {
position: relative;
height: 240px;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 18px 44px rgba(255, 130, 175, .35);
}
/* シーン本体 */
.sc-slide {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 36px;
opacity: 0;
visibility: hidden;
}
.sc-slide.is-active { opacity: 1; visibility: visible; }
/* 各シーンの桜系背景 */
.sc-slide[data-slide="0"] { background: radial-gradient(130% 130% at 25% 15%, #ffd1e0 0%, #ff8fb3 55%, #e75f93 100%); }
.sc-slide[data-slide="1"] { background: radial-gradient(130% 130% at 75% 20%, #d8b4ff 0%, #9a6bd6 55%, #4a2f78 100%); }
.sc-slide[data-slide="2"] { background: radial-gradient(130% 130% at 35% 80%, #ffe0ee 0%, #ff9ec2 50%, #ff6fa3 100%); }
.sc-kicker { font-size: 11px; letter-spacing: .32em; font-weight: 800; opacity: .9; margin-bottom: 10px; text-shadow: 0 1px 6px rgba(0,0,0,.2); }
.sc-title { margin: 0 0 10px; font-size: 30px; line-height: 1.1; text-shadow: 0 2px 10px rgba(0,0,0,.18); }
.sc-text { margin: 0; max-width: 36ch; font-size: 13px; line-height: 1.8; color: rgba(255,255,255,.95); text-shadow: 0 1px 6px rgba(0,0,0,.2); }
/* カーテンパネル(白い幕) */
.sc-panel {
position: absolute;
left: 0;
width: 100%;
height: 50%;
background: linear-gradient(var(--curtain), #fff0f6);
transform: scaleY(0);
z-index: 5;
}
.sc-panel--top { top: 0; transform-origin: top; }
.sc-panel--bottom { bottom: 0; transform-origin: bottom; }
.sc-screen.is-closing .sc-panel { animation: scCurtain var(--dur) var(--ease) both; }
@keyframes scCurtain {
0% { transform: scaleY(0); }
45% { transform: scaleY(1); }
55% { transform: scaleY(1); }
100% { transform: scaleY(0); }
}
/* 操作UI */
.sc-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 16px;
}
.sc-nav {
width: 38px; height: 38px;
border-radius: 50%;
border: 1px solid #ffc6da;
background: #fff;
color: #ff6fa3;
font-size: 18px;
cursor: pointer;
transition: background .2s ease, transform .15s ease;
}
.sc-nav:hover { background: #ffeef4; transform: scale(1.06); }
.sc-nav:focus-visible { outline: 2px solid #ff8fb3; outline-offset: 2px; }
.sc-dots { display: flex; gap: 9px; }
.sc-dot {
width: 9px; height: 9px;
border-radius: 50%;
border: none;
background: #ffc6da;
cursor: pointer;
padding: 0;
transition: background .25s ease, transform .25s ease;
}
.sc-dot.is-on { background: #ff6fa3; transform: scale(1.3); }
.sc-dot:focus-visible { outline: 2px solid #ff8fb3; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.sc-screen.is-closing .sc-panel { animation-duration: 1ms; }
}
JavaScript
// Sakura ライブシーンをカーテン遷移で切替(幕が閉じ切る瞬間に差し替え)
(() => {
const screen = document.querySelector('.sc-screen');
const slides = [...document.querySelectorAll('.sc-slide')];
const dots = [...document.querySelectorAll('.sc-dot')];
if (!screen || slides.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let current = 0;
let busy = false; // 遷移中の多重起動を防止
const apply = (index) => {
slides.forEach((s, i) => s.classList.toggle('is-active', i === index));
dots.forEach((d, i) => d.classList.toggle('is-on', i === index));
};
const goTo = (index) => {
const next = (index + slides.length) % slides.length;
if (busy || next === current) return;
busy = true;
// モーション低減時は即時切替
if (reduce) {
current = next;
apply(current);
busy = false;
return;
}
screen.classList.add('is-closing');
// 幕が閉じ切る中央タイミングで裏のシーンを差し替える
window.setTimeout(() => {
current = next;
apply(current);
}, 300);
};
// アニメ終了でフラグ解除&クラス除去
screen.addEventListener('animationend', (e) => {
if (!e.target.classList.contains('sc-panel')) return;
screen.classList.remove('is-closing');
busy = false;
});
// 前後ナビ/ドット
document.querySelector('.sc-controls')?.addEventListener('click', (e) => {
const nav = e.target.closest('.sc-nav');
const dot = e.target.closest('.sc-dot');
if (nav) goTo(current + Number(nav.dataset.dir));
if (dot) goTo(Number(dot.dataset.go));
});
// 自動再生(5秒ごと、操作でリセット)
let timer = window.setInterval(() => goTo(current + 1), 5000);
document.querySelector('.sc-app')?.addEventListener('click', () => {
window.clearInterval(timer);
timer = window.setInterval(() => goTo(current + 1), 5000);
});
})();
コード
HTML
<!-- カーテン遷移: パネルが上下から閉じ、その間に裏でコンテンツを差し替える -->
<div class="cur-stage">
<div class="cur-screen">
<!-- 表示中のスライド -->
<article class="cur-slide is-active" data-slide="0">
<span class="cur-kicker">SCENE 01</span>
<h2 class="cur-title">Midnight Drive</h2>
<p class="cur-text">ネオンが滲む夜の高速。深いブルーのグラデーションが奥行きを生む。</p>
</article>
<article class="cur-slide" data-slide="1">
<span class="cur-kicker">SCENE 02</span>
<h2 class="cur-title">Solar Flare</h2>
<p class="cur-text">熱を帯びたオレンジの渦。視線を中心へ集める放射状の光。</p>
</article>
<article class="cur-slide" data-slide="2">
<span class="cur-kicker">SCENE 03</span>
<h2 class="cur-title">Emerald Mist</h2>
<p class="cur-text">霧に沈む森の緑。静けさを湛えた寒色のレイヤー。</p>
</article>
<!-- カーテン(上下2枚) -->
<span class="cur-panel cur-panel--top" aria-hidden="true"></span>
<span class="cur-panel cur-panel--bottom" aria-hidden="true"></span>
</div>
<div class="cur-controls">
<button class="cur-nav" data-dir="-1" aria-label="前のシーン">‹</button>
<div class="cur-dots" role="tablist">
<button class="cur-dot is-on" data-go="0" aria-label="シーン1"></button>
<button class="cur-dot" data-go="1" aria-label="シーン2"></button>
<button class="cur-dot" data-go="2" aria-label="シーン3"></button>
</div>
<button class="cur-nav" data-dir="1" aria-label="次のシーン">›</button>
</div>
</div>
CSS
* { box-sizing: border-box; }
:root {
--text: #f4f6ff;
--curtain: #0c0d16;
--dur: 560ms;
--ease: cubic-bezier(.76, 0, .24, 1);
}
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: #05060c;
}
.cur-stage { width: min(680px, 94vw); }
.cur-screen {
position: relative;
height: 260px;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,.55);
}
/* スライド本体 */
.cur-slide {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 40px;
opacity: 0;
visibility: hidden;
}
.cur-slide.is-active { opacity: 1; visibility: visible; }
/* 各シーンの背景 */
.cur-slide[data-slide="0"] { background: radial-gradient(120% 120% at 20% 10%, #2b4cff 0%, #0a1030 55%, #05060c 100%); }
.cur-slide[data-slide="1"] { background: radial-gradient(120% 120% at 80% 20%, #ff8a3d 0%, #b81e4a 55%, #2a0613 100%); }
.cur-slide[data-slide="2"] { background: radial-gradient(120% 120% at 30% 80%, #2af598 0%, #0d6b6b 55%, #04201f 100%); }
.cur-kicker {
font-size: 12px;
letter-spacing: .35em;
font-weight: 700;
opacity: .8;
margin-bottom: 10px;
}
.cur-title { margin: 0 0 10px; font-size: 34px; line-height: 1.05; }
.cur-text { margin: 0; max-width: 38ch; font-size: 14px; line-height: 1.7; color: rgba(255,255,255,.82); }
/* カーテンパネル */
.cur-panel {
position: absolute;
left: 0;
width: 100%;
height: 50%;
background: var(--curtain);
transform: scaleY(0);
z-index: 5;
}
.cur-panel--top { top: 0; transform-origin: top; }
.cur-panel--bottom { bottom: 0; transform-origin: bottom; }
/* 閉じる→開く一連の動き(中央で重なり、その隙にDOM差替) */
.cur-screen.is-closing .cur-panel { animation: curtainSweep var(--dur) var(--ease) both; }
@keyframes curtainSweep {
0% { transform: scaleY(0); }
45% { transform: scaleY(1); }
55% { transform: scaleY(1); }
100% { transform: scaleY(0); }
}
/* 操作UI */
.cur-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
margin-top: 18px;
}
.cur-nav {
width: 40px; height: 40px;
border-radius: 50%;
border: 1px solid #2b2f44;
background: #14172a;
color: var(--text);
font-size: 20px;
cursor: pointer;
transition: background .2s ease, transform .15s ease;
}
.cur-nav:hover { background: #1f2440; transform: scale(1.06); }
.cur-nav:focus-visible { outline: 2px solid #6c8cff; outline-offset: 2px; }
.cur-dots { display: flex; gap: 10px; }
.cur-dot {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: #3a3f5c;
cursor: pointer;
padding: 0;
transition: background .25s ease, transform .25s ease;
}
.cur-dot.is-on { background: #fff; transform: scale(1.25); }
.cur-dot:focus-visible { outline: 2px solid #6c8cff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.cur-screen.is-closing .cur-panel { animation-duration: 1ms; }
}
@media (max-width: 520px) {
.cur-title { font-size: 26px; }
.cur-slide { padding: 0 24px; }
}
JavaScript
// カーテン遷移: パネルが中央で閉じ切る瞬間に裏でスライドを切替
(() => {
const screen = document.querySelector('.cur-screen');
const slides = [...document.querySelectorAll('.cur-slide')];
const dots = [...document.querySelectorAll('.cur-dot')];
if (!screen || slides.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let current = 0;
let busy = false; // 遷移中の多重起動を防止
const apply = (index) => {
slides.forEach((s, i) => s.classList.toggle('is-active', i === index));
dots.forEach((d, i) => d.classList.toggle('is-on', i === index));
};
const goTo = (index) => {
const next = (index + slides.length) % slides.length;
if (busy || next === current) return;
busy = true;
// モーション低減時はカーテン演出を省き即時切替
if (reduce) {
current = next;
apply(current);
busy = false;
return;
}
screen.classList.add('is-closing');
// カーテンが閉じ切る中央タイミング(アニメ約56%)でDOMを差し替える
const swapAt = 300;
window.setTimeout(() => {
current = next;
apply(current);
}, swapAt);
};
// アニメ終了でフラグ解除&クラス除去(次回再生のため)
screen.addEventListener('animationend', (e) => {
if (!e.target.classList.contains('cur-panel')) return;
screen.classList.remove('is-closing');
busy = false;
});
// 前後ナビ
document.querySelector('.cur-controls')?.addEventListener('click', (e) => {
const nav = e.target.closest('.cur-nav');
const dot = e.target.closest('.cur-dot');
if (nav) goTo(current + Number(nav.dataset.dir));
if (dot) goTo(Number(dot.dataset.go));
});
// 自動再生(5秒ごと、操作で気にせず一定間隔)
let timer = window.setInterval(() => goTo(current + 1), 5000);
// 操作時はタイマーをリセットして直後の自動送りを防ぐ
screen.closest('.cur-stage')?.addEventListener('click', () => {
window.clearInterval(timer);
timer = window.setInterval(() => goTo(current + 1), 5000);
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「カーテン遷移スライダー」の効果を追加してください。
# 追加してほしい効果
カーテン遷移スライダー(ページ遷移 / View Transitions)
上下のパネルが中央で閉じ切る瞬間に裏でコンテンツを差し替えるカーテン演出。シーン切替やページ遷移のつなぎに使える劇的なトランジションです。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- カーテン遷移: パネルが上下から閉じ、その間に裏でコンテンツを差し替える -->
<div class="cur-stage">
<div class="cur-screen">
<!-- 表示中のスライド -->
<article class="cur-slide is-active" data-slide="0">
<span class="cur-kicker">SCENE 01</span>
<h2 class="cur-title">Midnight Drive</h2>
<p class="cur-text">ネオンが滲む夜の高速。深いブルーのグラデーションが奥行きを生む。</p>
</article>
<article class="cur-slide" data-slide="1">
<span class="cur-kicker">SCENE 02</span>
<h2 class="cur-title">Solar Flare</h2>
<p class="cur-text">熱を帯びたオレンジの渦。視線を中心へ集める放射状の光。</p>
</article>
<article class="cur-slide" data-slide="2">
<span class="cur-kicker">SCENE 03</span>
<h2 class="cur-title">Emerald Mist</h2>
<p class="cur-text">霧に沈む森の緑。静けさを湛えた寒色のレイヤー。</p>
</article>
<!-- カーテン(上下2枚) -->
<span class="cur-panel cur-panel--top" aria-hidden="true"></span>
<span class="cur-panel cur-panel--bottom" aria-hidden="true"></span>
</div>
<div class="cur-controls">
<button class="cur-nav" data-dir="-1" aria-label="前のシーン">‹</button>
<div class="cur-dots" role="tablist">
<button class="cur-dot is-on" data-go="0" aria-label="シーン1"></button>
<button class="cur-dot" data-go="1" aria-label="シーン2"></button>
<button class="cur-dot" data-go="2" aria-label="シーン3"></button>
</div>
<button class="cur-nav" data-dir="1" aria-label="次のシーン">›</button>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
:root {
--text: #f4f6ff;
--curtain: #0c0d16;
--dur: 560ms;
--ease: cubic-bezier(.76, 0, .24, 1);
}
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: #05060c;
}
.cur-stage { width: min(680px, 94vw); }
.cur-screen {
position: relative;
height: 260px;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,.55);
}
/* スライド本体 */
.cur-slide {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 40px;
opacity: 0;
visibility: hidden;
}
.cur-slide.is-active { opacity: 1; visibility: visible; }
/* 各シーンの背景 */
.cur-slide[data-slide="0"] { background: radial-gradient(120% 120% at 20% 10%, #2b4cff 0%, #0a1030 55%, #05060c 100%); }
.cur-slide[data-slide="1"] { background: radial-gradient(120% 120% at 80% 20%, #ff8a3d 0%, #b81e4a 55%, #2a0613 100%); }
.cur-slide[data-slide="2"] { background: radial-gradient(120% 120% at 30% 80%, #2af598 0%, #0d6b6b 55%, #04201f 100%); }
.cur-kicker {
font-size: 12px;
letter-spacing: .35em;
font-weight: 700;
opacity: .8;
margin-bottom: 10px;
}
.cur-title { margin: 0 0 10px; font-size: 34px; line-height: 1.05; }
.cur-text { margin: 0; max-width: 38ch; font-size: 14px; line-height: 1.7; color: rgba(255,255,255,.82); }
/* カーテンパネル */
.cur-panel {
position: absolute;
left: 0;
width: 100%;
height: 50%;
background: var(--curtain);
transform: scaleY(0);
z-index: 5;
}
.cur-panel--top { top: 0; transform-origin: top; }
.cur-panel--bottom { bottom: 0; transform-origin: bottom; }
/* 閉じる→開く一連の動き(中央で重なり、その隙にDOM差替) */
.cur-screen.is-closing .cur-panel { animation: curtainSweep var(--dur) var(--ease) both; }
@keyframes curtainSweep {
0% { transform: scaleY(0); }
45% { transform: scaleY(1); }
55% { transform: scaleY(1); }
100% { transform: scaleY(0); }
}
/* 操作UI */
.cur-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
margin-top: 18px;
}
.cur-nav {
width: 40px; height: 40px;
border-radius: 50%;
border: 1px solid #2b2f44;
background: #14172a;
color: var(--text);
font-size: 20px;
cursor: pointer;
transition: background .2s ease, transform .15s ease;
}
.cur-nav:hover { background: #1f2440; transform: scale(1.06); }
.cur-nav:focus-visible { outline: 2px solid #6c8cff; outline-offset: 2px; }
.cur-dots { display: flex; gap: 10px; }
.cur-dot {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: #3a3f5c;
cursor: pointer;
padding: 0;
transition: background .25s ease, transform .25s ease;
}
.cur-dot.is-on { background: #fff; transform: scale(1.25); }
.cur-dot:focus-visible { outline: 2px solid #6c8cff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
.cur-screen.is-closing .cur-panel { animation-duration: 1ms; }
}
@media (max-width: 520px) {
.cur-title { font-size: 26px; }
.cur-slide { padding: 0 24px; }
}
【JavaScript】
// カーテン遷移: パネルが中央で閉じ切る瞬間に裏でスライドを切替
(() => {
const screen = document.querySelector('.cur-screen');
const slides = [...document.querySelectorAll('.cur-slide')];
const dots = [...document.querySelectorAll('.cur-dot')];
if (!screen || slides.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let current = 0;
let busy = false; // 遷移中の多重起動を防止
const apply = (index) => {
slides.forEach((s, i) => s.classList.toggle('is-active', i === index));
dots.forEach((d, i) => d.classList.toggle('is-on', i === index));
};
const goTo = (index) => {
const next = (index + slides.length) % slides.length;
if (busy || next === current) return;
busy = true;
// モーション低減時はカーテン演出を省き即時切替
if (reduce) {
current = next;
apply(current);
busy = false;
return;
}
screen.classList.add('is-closing');
// カーテンが閉じ切る中央タイミング(アニメ約56%)でDOMを差し替える
const swapAt = 300;
window.setTimeout(() => {
current = next;
apply(current);
}, swapAt);
};
// アニメ終了でフラグ解除&クラス除去(次回再生のため)
screen.addEventListener('animationend', (e) => {
if (!e.target.classList.contains('cur-panel')) return;
screen.classList.remove('is-closing');
busy = false;
});
// 前後ナビ
document.querySelector('.cur-controls')?.addEventListener('click', (e) => {
const nav = e.target.closest('.cur-nav');
const dot = e.target.closest('.cur-dot');
if (nav) goTo(current + Number(nav.dataset.dir));
if (dot) goTo(Number(dot.dataset.go));
});
// 自動再生(5秒ごと、操作で気にせず一定間隔)
let timer = window.setInterval(() => goTo(current + 1), 5000);
// 操作時はタイマーをリセットして直後の自動送りを防ぐ
screen.closest('.cur-stage')?.addEventListener('click', () => {
window.clearInterval(timer);
timer = window.setInterval(() => goTo(current + 1), 5000);
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。