スクロール文章ハイライト
スクロール進捗に合わせて文章を単語ごとに点灯させ、読み手の視線を導きます。引用や物語の見せ場に。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura 新曲歌詞ページ。スクロールで歌詞が一語ずつ点灯 -->
<div class="skt-scroller" id="thScroller">
<header class="skt-head">
<span class="skt-kicker">NEW SONG / LYRICS</span>
<h1>はなびらメロディ</h1>
<p class="skt-credit">作詞 あおい ・ 作曲 Sakura Sound</p>
<span class="skt-arrow" aria-hidden="true">↓</span>
</header>
<section class="skt-stage">
<!-- スクロール進捗に合わせて単語が点灯する歌詞 -->
<p class="skt-lyrics" id="thText">
<span class="th-word">風が</span> <span class="th-word">運ぶ</span> <span class="th-word">春の</span> <span class="th-word">合図</span> <span class="th-word">きみと</span> <span class="th-word">見上げた</span> <span class="th-word">空に</span> <span class="th-word">舞う</span> <span class="th-word">はなびら</span> <span class="th-word">ひとつ</span> <span class="th-word">ふたつ</span> <span class="th-word">数えた</span> <span class="th-word">あの日の</span> <span class="th-word">続きを</span> <span class="th-word">いま</span> <span class="th-word">歌うよ</span> <span class="th-word">ずっと</span> <span class="th-word">忘れない</span> <span class="th-word">この</span> <span class="th-word">メロディ</span>
</p>
</section>
<footer class="skt-foot">
<p>Sakura 3rd Single「はなびらメロディ」4月リリース。</p>
</footer>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--pink: #ffd1e0;
--pink-deep: #ff8fb3;
--gray: #eef0f3;
--ink: #5a4853;
}
body {
font-family: "Hiragino Mincho ProN", "Yu Mincho", "Segoe UI", serif;
background: #fff;
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* 自前スクロール領域 */
.skt-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--pink-deep) transparent;
background: linear-gradient(180deg, #fff 0%, #fff5f9 100%);
}
.skt-head {
height: 220px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
background: linear-gradient(180deg, var(--pink), #fff5f9);
}
.skt-kicker { font-family: "Segoe UI", sans-serif; font-size: .62rem; letter-spacing: .28em; color: var(--pink-deep); }
.skt-head h1 { font-size: 2rem; font-weight: 700; letter-spacing: .08em; color: #c4396a; }
.skt-credit { font-size: .76rem; color: #9a7080; }
.skt-arrow { margin-top: 2px; font-size: 1.3rem; color: var(--pink-deep); animation: sktBob 1.6s ease-in-out infinite; }
@keyframes sktBob {
0%,100% { transform: translateY(0); opacity: .6; }
50% { transform: translateY(8px); opacity: 1; }
}
/* 歌詞ステージ */
.skt-stage {
min-height: 130vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 28px;
}
.skt-lyrics {
max-width: 460px;
font-size: 1.5rem;
line-height: 2.1;
font-weight: 600;
letter-spacing: .02em;
}
/* 初期は淡色。点灯すると桜色で浮かび上がる */
.th-word {
color: #e7cdd6;
transition: color .35s ease, text-shadow .35s ease;
}
.th-word.lit {
color: #c4396a;
text-shadow: 0 0 18px rgba(255,143,179,.55);
}
.skt-foot {
text-align: center;
padding: 24px 24px 80px;
}
.skt-foot p { font-family: "Segoe UI", sans-serif; font-size: .76rem; color: var(--pink-deep); letter-spacing: .06em; }
@media (prefers-reduced-motion: reduce) {
.th-word { transition: none; }
.skt-arrow { animation: none; }
}
JavaScript
// Sakura 歌詞:スクロール通過進捗に応じて単語を一語ずつ点灯
(() => {
const scroller = document.getElementById('thScroller');
const text = document.getElementById('thText');
const words = text ? Array.from(text.querySelectorAll('.th-word')) : [];
if (!scroller || !text || !words.length) return; // null安全
function render() {
const vh = scroller.clientHeight;
// text の上端をスクロール領域の座標系で求める
const top = text.offsetTop - scroller.scrollTop;
// 文章が枠下から入り、上に抜けるまでを 0〜1 に正規化
const start = vh * 0.85;
const end = vh * 0.15;
let progress = (start - top) / (start - end + text.offsetHeight);
progress = Math.min(Math.max(progress, 0), 1);
const litCount = Math.round(progress * words.length);
words.forEach((w, i) => w.classList.toggle('lit', i < litCount));
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
};
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても点灯が伝わるよう、ゆっくり往復スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
let dir = 1;
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 (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
}
})();
コード
HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="th-scroller" id="thScroller">
<div class="th-spacer">↓ スクロールで文章が色づく</div>
<!-- スクロールに合わせて単語が1つずつハイライトされる -->
<section class="th-section">
<p class="th-text" id="thText">
<span class="th-word">スクロール</span>
<span class="th-word">に</span>
<span class="th-word">あわせて</span>
<span class="th-word">言葉</span>
<span class="th-word">が</span>
<span class="th-word">ひとつ</span>
<span class="th-word">ずつ</span>
<span class="th-word">明るく</span>
<span class="th-word">浮かび</span>
<span class="th-word">あがる。</span>
<span class="th-word">読み手</span>
<span class="th-word">の</span>
<span class="th-word">視線</span>
<span class="th-word">を</span>
<span class="th-word">やさしく</span>
<span class="th-word">導く</span>
<span class="th-word">演出。</span>
</p>
</section>
<div class="th-spacer th-end">✓ おわり</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--dim: #3a3a48;
--lit: #fff7e6;
--glow: #ffb454;
}
body {
font-family: "Hiragino Sans", "Yu Gothic", "Segoe UI", system-ui, sans-serif;
background: #15151d;
color: #fff;
}
/* プレビュー枠を埋める自前スクロール領域 */
.th-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.th-spacer {
height: 220px;
display: flex;
align-items: center;
justify-content: center;
color: #777;
font-size: .9rem;
letter-spacing: .08em;
}
.th-end { color: var(--glow); }
/* 文章は中央に大きく */
.th-section {
min-height: 120vh; /* 読み進める距離を確保 */
display: flex;
align-items: center;
justify-content: center;
padding: 0 26px;
}
.th-text {
max-width: 560px;
font-size: 1.7rem;
line-height: 1.9;
font-weight: 700;
}
.th-word {
color: var(--dim);
transition: color .35s ease, text-shadow .35s ease;
}
/* JSが付与する点灯クラス */
.th-word.lit {
color: var(--lit);
text-shadow: 0 0 18px rgba(255,180,84,.5);
}
@media (prefers-reduced-motion: reduce) {
.th-word { transition: none; }
}
JavaScript
// 自前スクロール領域の通過進捗に応じて単語を点灯
(() => {
const scroller = document.getElementById('thScroller');
const text = document.getElementById('thText');
const words = text ? Array.from(text.querySelectorAll('.th-word')) : [];
if (!scroller || !text || !words.length) return; // null安全
function render() {
const vh = scroller.clientHeight;
// text の上端をスクロール領域の座標系で求める
const top = text.offsetTop - scroller.scrollTop;
// 文章が枠下から入り、上に抜けるまでを 0〜1 に正規化
const start = vh * 0.85;
const end = vh * 0.15;
let progress = (start - top) / (start - end + text.offsetHeight);
progress = Math.min(Math.max(progress, 0), 1);
const litCount = Math.round(progress * words.length);
words.forEach((w, i) => w.classList.toggle('lit', i < litCount));
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
};
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても点灯が伝わるよう、ゆっくり往復スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
let dir = 1;
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 (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スクロール文章ハイライト」の効果を追加してください。
# 追加してほしい効果
スクロール文章ハイライト(スクロール演出)
スクロール進捗に合わせて文章を単語ごとに点灯させ、読み手の視線を導きます。引用や物語の見せ場に。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="th-scroller" id="thScroller">
<div class="th-spacer">↓ スクロールで文章が色づく</div>
<!-- スクロールに合わせて単語が1つずつハイライトされる -->
<section class="th-section">
<p class="th-text" id="thText">
<span class="th-word">スクロール</span>
<span class="th-word">に</span>
<span class="th-word">あわせて</span>
<span class="th-word">言葉</span>
<span class="th-word">が</span>
<span class="th-word">ひとつ</span>
<span class="th-word">ずつ</span>
<span class="th-word">明るく</span>
<span class="th-word">浮かび</span>
<span class="th-word">あがる。</span>
<span class="th-word">読み手</span>
<span class="th-word">の</span>
<span class="th-word">視線</span>
<span class="th-word">を</span>
<span class="th-word">やさしく</span>
<span class="th-word">導く</span>
<span class="th-word">演出。</span>
</p>
</section>
<div class="th-spacer th-end">✓ おわり</div>
</div>
【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--dim: #3a3a48;
--lit: #fff7e6;
--glow: #ffb454;
}
body {
font-family: "Hiragino Sans", "Yu Gothic", "Segoe UI", system-ui, sans-serif;
background: #15151d;
color: #fff;
}
/* プレビュー枠を埋める自前スクロール領域 */
.th-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
.th-spacer {
height: 220px;
display: flex;
align-items: center;
justify-content: center;
color: #777;
font-size: .9rem;
letter-spacing: .08em;
}
.th-end { color: var(--glow); }
/* 文章は中央に大きく */
.th-section {
min-height: 120vh; /* 読み進める距離を確保 */
display: flex;
align-items: center;
justify-content: center;
padding: 0 26px;
}
.th-text {
max-width: 560px;
font-size: 1.7rem;
line-height: 1.9;
font-weight: 700;
}
.th-word {
color: var(--dim);
transition: color .35s ease, text-shadow .35s ease;
}
/* JSが付与する点灯クラス */
.th-word.lit {
color: var(--lit);
text-shadow: 0 0 18px rgba(255,180,84,.5);
}
@media (prefers-reduced-motion: reduce) {
.th-word { transition: none; }
}
【JavaScript】
// 自前スクロール領域の通過進捗に応じて単語を点灯
(() => {
const scroller = document.getElementById('thScroller');
const text = document.getElementById('thText');
const words = text ? Array.from(text.querySelectorAll('.th-word')) : [];
if (!scroller || !text || !words.length) return; // null安全
function render() {
const vh = scroller.clientHeight;
// text の上端をスクロール領域の座標系で求める
const top = text.offsetTop - scroller.scrollTop;
// 文章が枠下から入り、上に抜けるまでを 0〜1 に正規化
const start = vh * 0.85;
const end = vh * 0.15;
let progress = (start - top) / (start - end + text.offsetHeight);
progress = Math.min(Math.max(progress, 0), 1);
const litCount = Math.round(progress * words.length);
words.forEach((w, i) => w.classList.toggle('lit', i < litCount));
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
};
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', render);
render(); // 初期化
// 操作がなくても点灯が伝わるよう、ゆっくり往復スクロール
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let auto = !reduce;
let dir = 1;
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 (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 700);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。