stickyスクロールストーリー
position:stickyでビジュアルを固定したまま、スクロールに連動して中身を切り替えるストーリーテリングUI。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk 機能紹介。左のビジュアルを固定し、右の説明をスクロールで切替 -->
<div class="fd-scroller" id="storyScroller">
<div class="fd-layout">
<!-- sticky で固定されるビジュアル -->
<aside class="fd-sticky">
<div class="fd-window">
<div class="fd-window-bar">
<span></span><span></span><span></span>
<em class="fd-window-title">FlowDesk</em>
</div>
<div class="fd-art" id="storyArt">
<span class="fd-emoji" id="storyEmoji">📊</span>
<span class="fd-art-label" id="storyLabel">STEP 1</span>
</div>
</div>
</aside>
<!-- スクロールで進む説明ステップ -->
<div class="fd-steps">
<section class="fd-step is-active" data-step="0" data-color="#0f1b34" data-emoji="📊">
<span class="fd-step-no">01</span>
<h2>すべての業務を1つに</h2>
<p>タスク、案件、メンバーの状況をひとつのダッシュボードへ。点在していた情報が、今日やることへと整理されます。</p>
</section>
<section class="fd-step" data-step="1" data-color="#16284a" data-emoji="⚡">
<span class="fd-step-no">02</span>
<h2>自動化でムダを削減</h2>
<p>「申請が来たら担当へ通知」など、繰り返しの作業はルールで自動化。チームは本質的な仕事に集中できます。</p>
</section>
<section class="fd-step" data-step="2" data-color="#1d3a6b" data-emoji="👥">
<span class="fd-step-no">03</span>
<h2>チームの今が見える</h2>
<p>誰が何を抱えているかを可視化。負荷の偏りに気づき、先回りで助け合える体制に。</p>
</section>
<section class="fd-step" data-step="3" data-color="#2451a6" data-emoji="🚀">
<span class="fd-step-no">04</span>
<h2>14日間、無料で試す</h2>
<p>クレジットカードは不要。まずは小さなチームから、FlowDesk のある毎日を体験してください。</p>
</section>
</div>
</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--navy: #0f1b34;
--blue: #4f7cff;
--white: #ffffff;
}
body {
font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
background: #0a1226;
color: var(--white);
-webkit-font-smoothing: antialiased;
}
/* 内部スクロール領域 */
.fd-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--blue) transparent;
background: #0a1226;
}
.fd-layout {
display: flex;
align-items: flex-start;
gap: 16px;
max-width: 640px;
margin: 0 auto;
padding: 0 18px;
}
/* 固定されるビジュアル */
.fd-sticky {
position: sticky;
top: 0;
flex: 0 0 46%;
height: 100vh;
max-height: 100%;
display: flex;
align-items: center;
}
.fd-window {
width: 100%;
border-radius: 14px;
overflow: hidden;
background: #0e1a33;
border: 1px solid rgba(79,124,255,.25);
box-shadow: 0 30px 60px -30px rgba(0,0,0,.9);
}
.fd-window-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 12px;
background: #0b1430;
border-bottom: 1px solid rgba(255,255,255,.06);
}
.fd-window-bar span { width: 8px; height: 8px; border-radius: 50%; background: #38456b; }
.fd-window-title { margin-left: 8px; font-size: .66rem; letter-spacing: .12em; color: #8ea3d6; font-style: normal; }
.fd-art {
position: relative;
height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: var(--navy);
transition: background .5s ease;
}
.fd-emoji {
font-size: 3rem;
line-height: 1;
transition: transform .4s cubic-bezier(.2,.8,.2,1);
}
.fd-art-label {
font-size: .62rem;
letter-spacing: .26em;
color: var(--blue);
font-weight: 700;
}
/* スクロールする説明側 */
.fd-steps {
flex: 1 1 auto;
padding: 60px 0;
}
.fd-step {
min-height: 70vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 18px 6px;
opacity: .35;
transform: translateY(10px);
transition: opacity .45s ease, transform .45s ease;
}
.fd-step.is-active { opacity: 1; transform: none; }
.fd-step-no {
font-size: .72rem;
font-weight: 800;
letter-spacing: .1em;
color: var(--blue);
}
.fd-step h2 { font-size: 1.3rem; font-weight: 800; margin: 8px 0 10px; }
.fd-step p { font-size: .9rem; line-height: 1.85; color: #b9c4e2; }
@media (prefers-reduced-motion: reduce) {
.fd-step { opacity: 1; transform: none; }
.fd-emoji, .fd-art { transition: none; }
}
JavaScript
// FlowDesk 機能紹介:中央に来たステップに合わせて固定ビジュアルを更新
(() => {
const scroller = document.getElementById('storyScroller');
const steps = Array.from(document.querySelectorAll('.fd-step'));
const art = document.getElementById('storyArt');
const emoji = document.getElementById('storyEmoji');
const label = document.getElementById('storyLabel');
if (!scroller || !steps.length) return; // null安全
function activate(step) {
steps.forEach(s => s.classList.toggle('is-active', s === step));
if (art) art.style.background = step.dataset.color || '#0f1b34';
if (emoji) {
emoji.innerHTML = step.dataset.emoji || '';
// 切替時に小さくバウンス
emoji.style.transform = 'scale(0.6)';
requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
}
if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.6 });
steps.forEach(s => io.observe(s));
}
activate(steps[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;
requestAnimationFrame(step);
}, 700);
}
})();
コード
HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="story-scroller" id="storyScroller">
<div class="sticky-story">
<!-- 左側:sticky で固定されるビジュアル -->
<div class="story-visual">
<div class="story-art" id="storyArt">
<span class="story-step-label" id="storyLabel">STEP 1</span>
<span class="story-emoji" id="storyEmoji">🌱</span>
</div>
</div>
<!-- 右側:スクロールするステップ群 -->
<div class="story-steps">
<section class="story-step" data-step="0" data-emoji="🌱" data-color="#2e7d4f">
<h2>種をまく</h2>
<p>小さな一粒から物語は始まります。土の中で静かに準備を整える時間。</p>
</section>
<section class="story-step" data-step="1" data-emoji="🌿" data-color="#3f8c5a">
<h2>芽が出る</h2>
<p>左のビジュアルがスクロールに連動して切り替わります。stickyで固定したまま中身だけ変化。</p>
</section>
<section class="story-step" data-step="2" data-emoji="🌼" data-color="#c9962b">
<h2>花が咲く</h2>
<p>各ステップが枠の中央に来た瞬間、IntersectionObserverが状態を更新します。</p>
</section>
<section class="story-step" data-step="3" data-emoji="🌳" data-color="#2e6b8c">
<h2>大きく育つ</h2>
<p>製品紹介・チュートリアル・ストーリーテリングに最適な定番レイアウト。</p>
</section>
</div>
</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: #f4f1ec;
color: #23231f;
}
/* プレビュー枠を埋める自前スクロール領域 */
.story-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.sticky-story {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 920px;
margin: 0 auto;
gap: 20px;
padding: 0 20px;
}
/* 左:固定ビジュアル(スクロール領域に貼り付く) */
.story-visual {
position: relative;
}
.story-art {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #2e7d4f; /* JSで上書き */
transition: background .6s ease;
}
.story-emoji {
font-size: 5rem;
filter: drop-shadow(0 8px 18px rgba(0,0,0,.25));
transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.story-step-label {
position: absolute;
top: 14px; left: 14px;
padding: 5px 12px;
font-size: .72rem;
font-weight: 800;
letter-spacing: .12em;
color: #fff;
background: rgba(0,0,0,.28);
border-radius: 999px;
z-index: 3;
pointer-events: none;
}
/* 右:スクロールするステップ */
.story-steps { padding: 40vh 0; }
.story-step {
min-height: 60vh;
display: flex;
flex-direction: column;
justify-content: center;
opacity: .35;
transition: opacity .5s ease;
}
.story-step.is-active { opacity: 1; }
.story-step h2 {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 10px;
}
.story-step p { line-height: 1.7; color: #4b4b44; font-size: .92rem; }
@media (prefers-reduced-motion: reduce) {
.story-art, .story-emoji, .story-step { transition: none; }
}
JavaScript
// 自前スクロール領域内で、中央に来たステップに合わせてビジュアルを更新
(() => {
const scroller = document.getElementById('storyScroller');
const steps = Array.from(document.querySelectorAll('.story-step'));
const art = document.getElementById('storyArt');
const emoji = document.getElementById('storyEmoji');
const label = document.getElementById('storyLabel');
if (!scroller || !steps.length) return; // null安全
function activate(step) {
steps.forEach(s => s.classList.toggle('is-active', s === step));
if (art) art.style.background = step.dataset.color || '#2e7d4f';
if (emoji) {
emoji.innerHTML = step.dataset.emoji || '';
// 切替時に小さくバウンス
emoji.style.transform = 'scale(0.6)';
requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
}
if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.6 });
steps.forEach(s => io.observe(s));
}
activate(steps[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;
requestAnimationFrame(step);
}, 700);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「stickyスクロールストーリー」の効果を追加してください。
# 追加してほしい効果
stickyスクロールストーリー(スクロール演出)
position:stickyでビジュアルを固定したまま、スクロールに連動して中身を切り替えるストーリーテリングUI。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="story-scroller" id="storyScroller">
<div class="sticky-story">
<!-- 左側:sticky で固定されるビジュアル -->
<div class="story-visual">
<div class="story-art" id="storyArt">
<span class="story-step-label" id="storyLabel">STEP 1</span>
<span class="story-emoji" id="storyEmoji">🌱</span>
</div>
</div>
<!-- 右側:スクロールするステップ群 -->
<div class="story-steps">
<section class="story-step" data-step="0" data-emoji="🌱" data-color="#2e7d4f">
<h2>種をまく</h2>
<p>小さな一粒から物語は始まります。土の中で静かに準備を整える時間。</p>
</section>
<section class="story-step" data-step="1" data-emoji="🌿" data-color="#3f8c5a">
<h2>芽が出る</h2>
<p>左のビジュアルがスクロールに連動して切り替わります。stickyで固定したまま中身だけ変化。</p>
</section>
<section class="story-step" data-step="2" data-emoji="🌼" data-color="#c9962b">
<h2>花が咲く</h2>
<p>各ステップが枠の中央に来た瞬間、IntersectionObserverが状態を更新します。</p>
</section>
<section class="story-step" data-step="3" data-emoji="🌳" data-color="#2e6b8c">
<h2>大きく育つ</h2>
<p>製品紹介・チュートリアル・ストーリーテリングに最適な定番レイアウト。</p>
</section>
</div>
</div>
</div>
【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: #f4f1ec;
color: #23231f;
}
/* プレビュー枠を埋める自前スクロール領域 */
.story-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.sticky-story {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 920px;
margin: 0 auto;
gap: 20px;
padding: 0 20px;
}
/* 左:固定ビジュアル(スクロール領域に貼り付く) */
.story-visual {
position: relative;
}
.story-art {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #2e7d4f; /* JSで上書き */
transition: background .6s ease;
}
.story-emoji {
font-size: 5rem;
filter: drop-shadow(0 8px 18px rgba(0,0,0,.25));
transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.story-step-label {
position: absolute;
top: 14px; left: 14px;
padding: 5px 12px;
font-size: .72rem;
font-weight: 800;
letter-spacing: .12em;
color: #fff;
background: rgba(0,0,0,.28);
border-radius: 999px;
z-index: 3;
pointer-events: none;
}
/* 右:スクロールするステップ */
.story-steps { padding: 40vh 0; }
.story-step {
min-height: 60vh;
display: flex;
flex-direction: column;
justify-content: center;
opacity: .35;
transition: opacity .5s ease;
}
.story-step.is-active { opacity: 1; }
.story-step h2 {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 10px;
}
.story-step p { line-height: 1.7; color: #4b4b44; font-size: .92rem; }
@media (prefers-reduced-motion: reduce) {
.story-art, .story-emoji, .story-step { transition: none; }
}
【JavaScript】
// 自前スクロール領域内で、中央に来たステップに合わせてビジュアルを更新
(() => {
const scroller = document.getElementById('storyScroller');
const steps = Array.from(document.querySelectorAll('.story-step'));
const art = document.getElementById('storyArt');
const emoji = document.getElementById('storyEmoji');
const label = document.getElementById('storyLabel');
if (!scroller || !steps.length) return; // null安全
function activate(step) {
steps.forEach(s => s.classList.toggle('is-active', s === step));
if (art) art.style.background = step.dataset.color || '#2e7d4f';
if (emoji) {
emoji.innerHTML = step.dataset.emoji || '';
// 切替時に小さくバウンス
emoji.style.transform = 'scale(0.6)';
requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
}
if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
}
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) activate(entry.target);
});
}, { root: scroller, threshold: 0.6 });
steps.forEach(s => io.observe(s));
}
activate(steps[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;
requestAnimationFrame(step);
}, 700);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。