ナラティブ・スクロール
スクロール進行で章(シーン)が切り替わり、背景色と見出しテキストがフェードで入れ替わる没入演出。3〜4シーンで物語のように読み進めるストーリーテリングUIです。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura 結成ストーリー。スクロールで章が切り替わり背景色と見出しが入れ替わる -->
<div class="skn-scroller" id="narScroller">
<!-- 固定背景:JSで色が切り替わる -->
<div class="skn-bg" id="narBg" aria-hidden="true"></div>
<!-- 固定の見出しステージ:各章の見出しがフェードで入れ替わる -->
<div class="skn-stage" aria-hidden="true">
<div class="skn-headlines" id="narHeadlines">
<h2 class="nar-headline">出会いの春</h2>
<h2 class="nar-headline">積み重ねた日々</h2>
<h2 class="nar-headline">はじめてのステージ</h2>
<h2 class="nar-headline">そして、いま</h2>
</div>
</div>
<!-- スクロールで通過する各章(高さでスクロール量を作る) -->
<section class="nar-scene skn-scene" data-scene="0" data-color="#ffd1e0">
<span class="skn-chapter">CHAPTER 01</span>
<p class="skn-caption">5人が出会ったのは、桜が満開の放課後だった。スクロールして物語を進めよう。</p>
</section>
<section class="nar-scene skn-scene" data-scene="1" data-color="#ffb6cd">
<span class="skn-chapter">CHAPTER 02</span>
<p class="skn-caption">レッスンの毎日。転んでも、また立ち上がって。</p>
</section>
<section class="nar-scene skn-scene" data-scene="2" data-color="#ff8fb3">
<span class="skn-chapter">CHAPTER 03</span>
<p class="skn-caption">小さな会場、満員のお客さん。緊張のなか、ライトが灯る。</p>
</section>
<section class="nar-scene skn-scene" data-scene="3" data-color="#ff6f9c">
<span class="skn-chapter">CHAPTER 04</span>
<p class="skn-caption">あの春から、わたしたちは Sakura になった。これからもよろしくね。</p>
</section>
</div>
CSS
:root {
--scene-bg: #ffd1e0; /* 現在の章の背景。JSで上書き */
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
color: #fff;
background: #ffd1e0;
}
/* 自前スクロール領域 */
.skn-scroller {
position: relative;
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: #ff6f9c transparent;
}
/* 固定背景:色がなめらかに切り替わる */
.skn-bg {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh; /* 後続シーンを上に重ねる */
background: var(--scene-bg);
transition: background 1s ease;
z-index: 0;
}
/* 花びらの淡い装飾を重ねる */
.skn-bg::after {
content: "";
position: absolute; inset: 0;
background-image:
radial-gradient(circle at 18% 25%, rgba(255,255,255,.5) 0 3px, transparent 4px),
radial-gradient(circle at 72% 40%, rgba(255,255,255,.4) 0 2px, transparent 3px),
radial-gradient(circle at 50% 80%, rgba(255,255,255,.35) 0 3px, transparent 4px);
background-size: 180px 180px;
opacity: .6;
}
/* 固定の見出しステージ */
.skn-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
}
.skn-headlines {
position: relative;
width: 100%;
text-align: center;
}
/* 見出しを重ねて配置し、active のみ表示 */
.nar-headline {
position: absolute;
left: 0; right: 0;
top: 50%;
font-size: clamp(1.8rem, 8vw, 3rem);
font-weight: 800;
letter-spacing: .08em;
color: #fff;
opacity: 0;
transform: translateY(calc(-50% + 24px));
transition: opacity .7s ease, transform .7s cubic-bezier(.2,.8,.2,1);
text-shadow: 0 6px 26px rgba(196,57,106,.45);
}
.nar-headline.is-active {
opacity: 1;
transform: translateY(-50%);
}
/* スクロール量を生む各章 */
.skn-scene {
position: relative;
z-index: 2;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding-bottom: 12%;
}
.skn-chapter {
font-size: .66rem;
letter-spacing: .3em;
color: #fff;
background: rgba(196,57,106,.35);
padding: 4px 14px;
border-radius: 999px;
}
.skn-caption {
max-width: 78%;
text-align: center;
font-size: .88rem;
line-height: 1.8;
color: #fff;
background: rgba(196,57,106,.28);
padding: 10px 16px;
border-radius: 12px;
backdrop-filter: blur(2px);
}
@media (prefers-reduced-motion: reduce) {
.skn-bg, .nar-headline { transition: none; }
}
JavaScript
// Sakura 結成ストーリー:中央に来た章に合わせて背景色と見出しを切り替える
(() => {
const scroller = document.getElementById('narScroller');
const bg = document.getElementById('narBg');
const scenes = Array.from(document.querySelectorAll('.nar-scene'));
const headlines = Array.from(document.querySelectorAll('.nar-headline'));
if (!scroller || !bg || !scenes.length) return; // null安全
function activate(scene) {
const i = Number(scene.dataset.scene) || 0;
// 背景色を切替
bg.style.setProperty('--scene-bg', scene.dataset.color || '#ffd1e0');
// 対応する見出しのみ表示
headlines.forEach((h, idx) => h.classList.toggle('is-active', idx === i));
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.55 });
scenes.forEach(s => io.observe(s));
}
activate(scenes[0]); // 初期章
// 操作がなくても章の切替が見えるよう、ゆっくり自動スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (scroller.scrollTop >= max - 1) return;
scroller.scrollTop += 2.2;
requestAnimationFrame(step);
}, 800);
}
})();
コード
HTML
<!-- 自前スクロール領域(プレビュー枠内で完結) -->
<div class="nar-scroller" id="narScroller">
<!-- 固定背景:JSで色が切り替わる -->
<div class="nar-bg" id="narBg" aria-hidden="true"></div>
<!-- 固定の見出しステージ:各シーンの見出しがフェードで入れ替わる -->
<div class="nar-stage" aria-hidden="true">
<div class="nar-headlines" id="narHeadlines">
<h2 class="nar-headline">夜明け前</h2>
<h2 class="nar-headline">光が差す</h2>
<h2 class="nar-headline">街が動き出す</h2>
<h2 class="nar-headline">日は高く昇る</h2>
</div>
</div>
<!-- スクロールで通過する各シーン(高さでスクロール量を作る) -->
<section class="nar-scene" data-scene="0" data-color="#101426" data-label="夜明け前">
<p class="nar-caption">物語はまだ眠りの中。スクロールして次の章へ。</p>
</section>
<section class="nar-scene" data-scene="1" data-color="#3a2c5e" data-label="光が差す"></section>
<section class="nar-scene" data-scene="2" data-color="#9c5a3a" data-label="街が動き出す"></section>
<section class="nar-scene" data-scene="3" data-color="#d99a3a" data-label="日は高く昇る">
<p class="nar-caption">章が切り替わるたび、背景と見出しが入れ替わります。</p>
</section>
</div>
CSS
:root {
--scene-bg: #101426; /* 現在シーンの背景。JSで上書き */
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
color: #fff;
background: #101426;
}
/* プレビュー枠を埋める自前スクロール領域 */
.nar-scroller {
position: relative;
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
/* 固定背景:色がなめらかに切り替わる */
.nar-bg {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh; /* 後続シーンを上に重ねる */
background: var(--scene-bg);
transition: background 1s ease;
z-index: 0;
}
/* 固定の見出しステージ */
.nar-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
}
/* 見出しを重ねて配置し、active のみ表示 */
.nar-headlines {
position: relative;
width: 100%;
text-align: center;
}
.nar-headline {
position: absolute;
left: 0;
right: 0;
top: 50%;
font-size: clamp(1.8rem, 8vw, 3.4rem);
font-weight: 800;
letter-spacing: .06em;
opacity: 0;
transform: translateY(calc(-50% + 24px));
transition: opacity .7s ease, transform .7s cubic-bezier(.2,.8,.2,1);
text-shadow: 0 6px 26px rgba(0, 0, 0, .35);
}
.nar-headline.is-active {
opacity: 1;
transform: translateY(-50%);
}
/* スクロール量を生むシーン */
.nar-scene {
position: relative;
z-index: 2;
height: 100vh;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 12%;
}
.nar-caption {
max-width: 80%;
text-align: center;
font-size: .9rem;
line-height: 1.7;
color: rgba(255, 255, 255, .8);
background: rgba(0, 0, 0, .22);
padding: 10px 16px;
border-radius: 12px;
backdrop-filter: blur(2px);
}
@media (prefers-reduced-motion: reduce) {
.nar-bg, .nar-headline { transition: none; }
}
JavaScript
// 自前スクロール領域内で、中央に来たシーンに合わせて背景色と見出しを切り替える
(() => {
const scroller = document.getElementById('narScroller');
const bg = document.getElementById('narBg');
const scenes = Array.from(document.querySelectorAll('.nar-scene'));
const headlines = Array.from(document.querySelectorAll('.nar-headline'));
if (!scroller || !bg || !scenes.length) return; // null安全
function activate(scene) {
const i = Number(scene.dataset.scene) || 0;
// 背景色を切替
bg.style.setProperty('--scene-bg', scene.dataset.color || '#101426');
// 対応する見出しのみ表示
headlines.forEach((h, idx) => h.classList.toggle('is-active', idx === i));
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.55 });
scenes.forEach(s => io.observe(s));
}
activate(scenes[0]); // 初期シーン
// 操作がなくても章の切替が見えるよう、ゆっくり自動スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (scroller.scrollTop >= max - 1) return;
scroller.scrollTop += 2.2;
requestAnimationFrame(step);
}, 800);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ナラティブ・スクロール」の効果を追加してください。
# 追加してほしい効果
ナラティブ・スクロール(スクロール演出)
スクロール進行で章(シーン)が切り替わり、背景色と見出しテキストがフェードで入れ替わる没入演出。3〜4シーンで物語のように読み進めるストーリーテリングUIです。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前スクロール領域(プレビュー枠内で完結) -->
<div class="nar-scroller" id="narScroller">
<!-- 固定背景:JSで色が切り替わる -->
<div class="nar-bg" id="narBg" aria-hidden="true"></div>
<!-- 固定の見出しステージ:各シーンの見出しがフェードで入れ替わる -->
<div class="nar-stage" aria-hidden="true">
<div class="nar-headlines" id="narHeadlines">
<h2 class="nar-headline">夜明け前</h2>
<h2 class="nar-headline">光が差す</h2>
<h2 class="nar-headline">街が動き出す</h2>
<h2 class="nar-headline">日は高く昇る</h2>
</div>
</div>
<!-- スクロールで通過する各シーン(高さでスクロール量を作る) -->
<section class="nar-scene" data-scene="0" data-color="#101426" data-label="夜明け前">
<p class="nar-caption">物語はまだ眠りの中。スクロールして次の章へ。</p>
</section>
<section class="nar-scene" data-scene="1" data-color="#3a2c5e" data-label="光が差す"></section>
<section class="nar-scene" data-scene="2" data-color="#9c5a3a" data-label="街が動き出す"></section>
<section class="nar-scene" data-scene="3" data-color="#d99a3a" data-label="日は高く昇る">
<p class="nar-caption">章が切り替わるたび、背景と見出しが入れ替わります。</p>
</section>
</div>
【CSS】
:root {
--scene-bg: #101426; /* 現在シーンの背景。JSで上書き */
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
color: #fff;
background: #101426;
}
/* プレビュー枠を埋める自前スクロール領域 */
.nar-scroller {
position: relative;
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
/* 固定背景:色がなめらかに切り替わる */
.nar-bg {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh; /* 後続シーンを上に重ねる */
background: var(--scene-bg);
transition: background 1s ease;
z-index: 0;
}
/* 固定の見出しステージ */
.nar-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
margin-bottom: -100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
}
/* 見出しを重ねて配置し、active のみ表示 */
.nar-headlines {
position: relative;
width: 100%;
text-align: center;
}
.nar-headline {
position: absolute;
left: 0;
right: 0;
top: 50%;
font-size: clamp(1.8rem, 8vw, 3.4rem);
font-weight: 800;
letter-spacing: .06em;
opacity: 0;
transform: translateY(calc(-50% + 24px));
transition: opacity .7s ease, transform .7s cubic-bezier(.2,.8,.2,1);
text-shadow: 0 6px 26px rgba(0, 0, 0, .35);
}
.nar-headline.is-active {
opacity: 1;
transform: translateY(-50%);
}
/* スクロール量を生むシーン */
.nar-scene {
position: relative;
z-index: 2;
height: 100vh;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 12%;
}
.nar-caption {
max-width: 80%;
text-align: center;
font-size: .9rem;
line-height: 1.7;
color: rgba(255, 255, 255, .8);
background: rgba(0, 0, 0, .22);
padding: 10px 16px;
border-radius: 12px;
backdrop-filter: blur(2px);
}
@media (prefers-reduced-motion: reduce) {
.nar-bg, .nar-headline { transition: none; }
}
【JavaScript】
// 自前スクロール領域内で、中央に来たシーンに合わせて背景色と見出しを切り替える
(() => {
const scroller = document.getElementById('narScroller');
const bg = document.getElementById('narBg');
const scenes = Array.from(document.querySelectorAll('.nar-scene'));
const headlines = Array.from(document.querySelectorAll('.nar-headline'));
if (!scroller || !bg || !scenes.length) return; // null安全
function activate(scene) {
const i = Number(scene.dataset.scene) || 0;
// 背景色を切替
bg.style.setProperty('--scene-bg', scene.dataset.color || '#101426');
// 対応する見出しのみ表示
headlines.forEach((h, idx) => h.classList.toggle('is-active', idx === i));
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.55 });
scenes.forEach(s => io.observe(s));
}
activate(scenes[0]); // 初期シーン
// 操作がなくても章の切替が見えるよう、ゆっくり自動スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (scroller.scrollTop >= max - 1) return;
scroller.scrollTop += 2.2;
requestAnimationFrame(step);
}, 800);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。