追従セクションタブ
ページ内セクションへのタブが、スクロールで上部に貼り付き、表示中の区画をハイライトします。商品詳細の「概要/仕様/レビュー」のような長いページの章内ナビとして定番です。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:機能詳細ページの追従セクションタブ -->
<div class="sst-frame">
<div class="sst-scroll" id="sstScroll">
<div class="sst-intro">
<p class="sst-cat">FEATURE</p>
<h1>自動レポート機能</h1>
</div>
<div class="sst-tabs" id="sstTabs">
<button class="sst-tab is-active" type="button" data-to="sec-o">概要</button>
<button class="sst-tab" type="button" data-to="sec-s">できること</button>
<button class="sst-tab" type="button" data-to="sec-r">料金</button>
<button class="sst-tab" type="button" data-to="sec-q">FAQ</button>
<span class="sst-ink" id="sstInk" aria-hidden="true"></span>
</div>
<section class="sst-sec" id="sec-o"><h2>概要</h2><p>プロジェクトの進捗を自動で集計し、定例レポートを生成します。タブは上部に貼り付き、読んでいる区画をハイライトします。</p><p>手作業の集計をゼロに近づけます。</p></section>
<section class="sst-sec" id="sec-s"><h2>できること</h2><p>週次・月次の自動配信、カスタム指標、PDF/CSV出力に対応。</p><p>テンプレートから数クリックで設定できます。</p></section>
<section class="sst-sec" id="sec-r"><h2>料金</h2><p>Pro プラン以上で利用可能。追加費用はかかりません。</p><p>14日間の無料トライアルで全機能をお試しいただけます。</p></section>
<section class="sst-sec" id="sec-q"><h2>FAQ</h2><p>Q. 既存データも集計できますか? A. はい、過去分も取り込めます。</p><p>Q. 配信先は? A. メール・Slack・Webhookに対応します。</p></section>
</div>
</div>
CSS
/* FlowDesk(SaaS):追従セクションタブの再スキン */
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.sst-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #fbfcff; }
.sst-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; }
.sst-intro { padding: 22px 26px 14px; }
.sst-cat { margin: 0 0 6px; font-size: 11px; letter-spacing: .22em; font-weight: 700; color: #3b5bff; }
.sst-intro h1 { margin: 0; font-size: 24px; font-weight: 800; color: #1f2547; }
.sst-tabs { position: sticky; top: 0; z-index: 5; display: flex; gap: 2px; padding: 0 18px; background: rgba(251,252,255,.92); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid #e7eaf6; }
.sst-tab { position: relative; font: inherit; font-size: 13.5px; font-weight: 600; color: #6b7290; background: none; border: none; cursor: pointer; padding: 13px 14px; transition: color .2s ease; }
.sst-tab:hover { color: #3a4060; }
.sst-tab.is-active { color: #3b5bff; }
.sst-ink { position: absolute; bottom: -1px; left: 0; width: 0; height: 2.5px; border-radius: 3px; background: #3b5bff; transition: left .3s cubic-bezier(.2,.8,.2,1), width .3s cubic-bezier(.2,.8,.2,1); }
.sst-sec { padding: 18px 26px; min-height: 150px; color: #3a4060; line-height: 1.85; border-bottom: 1px solid #eef0f8; }
.sst-sec:last-child { min-height: 220px; }
.sst-sec h2 { margin: 0 0 10px; font-size: 18px; font-weight: 800; color: #1f2547; }
.sst-sec p { margin: 0 0 12px; font-size: 14px; max-width: 520px; }
@media (prefers-reduced-motion: reduce) { .sst-ink, .sst-tab { transition: none; } }
JavaScript
// (デモと同じフックを流用)貼り付くタブの現在地ハイライト+下線移動+自動スクロール
(() => {
const sc = document.getElementById('sstScroll');
const tabsWrap = document.getElementById('sstTabs');
const ink = document.getElementById('sstInk');
if (!sc || !tabsWrap || !ink) return;
const tabs = [...tabsWrap.querySelectorAll('.sst-tab')];
const secs = tabs.map(t => sc.querySelector('#' + t.dataset.to)).filter(Boolean);
const TABS_H = 46;
function moveInk(tab) { ink.style.left = tab.offsetLeft + 'px'; ink.style.width = tab.offsetWidth + 'px'; }
function setActive(idx) { tabs.forEach((t, i) => t.classList.toggle('is-active', i === idx)); moveInk(tabs[idx]); }
tabs.forEach((t, i) => t.addEventListener('click', () => { auto = false; if (secs[i]) sc.scrollTo({ top: secs[i].offsetTop - TABS_H, behavior: 'smooth' }); }));
function spy() { const pos = sc.scrollTop + TABS_H + 10; let idx = 0; secs.forEach((s, i) => { if (s.offsetTop <= pos) idx = i; }); setActive(idx); }
let ticking = false;
sc.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { spy(); ticking = false; }); }, { passive: true });
requestAnimationFrame(() => setActive(0));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches, dir = 1;
['wheel', 'touchstart', 'pointerdown'].forEach(ev => sc.addEventListener(ev, () => { auto = false; }, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = sc.scrollHeight - sc.clientHeight;
sc.scrollTop += dir * 1.5;
if (sc.scrollTop >= max - 1) dir = -1; else if (sc.scrollTop <= 0) dir = 1;
requestAnimationFrame(step);
}, 1100);
}
})();
実装ガイド
使いどころ
商品詳細やドキュメントなど長いページの章内ナビに。セクションへのタブが上部に貼り付き、表示中の区画をハイライトします。
実装時の注意点
タブは position:sticky で上端に固定。各セクションの offsetTop と scrollTop を比較して現在地を判定し、下線インジケーターを移動します。クリックで該当位置へ smooth スクロール。
対応ブラウザ
position:sticky・smooth scroll・offset 計測は全モダンブラウザ対応。
よくある失敗
固定タブの高さ分をスクロール位置の計算(とジャンプ先)から引かないと見出しが隠れます。セクションが短いと現在地が頻繁に切り替わるので判定にオフセットを。レスポンシブでタブが折り返す場合の下線位置に注意します。
応用例
IntersectionObserver による厳密なハイライト、タブ横スクロール対応、現在地のプログレス表示、URLハッシュ連動などに展開できます。
コード
HTML
<!-- スクロールで上部に貼り付くセクションタブ(現在地ハイライト) -->
<div class="sst-frame">
<div class="sst-scroll" id="sstScroll">
<div class="sst-intro">
<p class="sst-cat">PRODUCT</p>
<h1>ワイヤレスイヤホン X</h1>
</div>
<div class="sst-tabs" id="sstTabs">
<button class="sst-tab is-active" type="button" data-to="sec-o">概要</button>
<button class="sst-tab" type="button" data-to="sec-s">仕様</button>
<button class="sst-tab" type="button" data-to="sec-r">レビュー</button>
<button class="sst-tab" type="button" data-to="sec-q">Q&A</button>
<span class="sst-ink" id="sstInk" aria-hidden="true"></span>
</div>
<section class="sst-sec" id="sec-o"><h2>概要</h2><p>軽量設計と高音質を両立したフラッグシップ。タブは上部に貼り付き、読んでいる区画をハイライトします。</p><p>長い商品ページの章内ナビとして定番の挙動です。</p></section>
<section class="sst-sec" id="sec-s"><h2>仕様</h2><p>連続再生8時間、ケース併用で32時間。Bluetooth 5.3、IPX4防滴対応。</p><p>対応コーデック:SBC / AAC / LDAC。</p></section>
<section class="sst-sec" id="sec-r"><h2>レビュー</h2><p>★4.7(1,204件)。「装着感が軽い」「通話がクリア」と高評価です。</p><p>低音の量感と解像度のバランスが好評です。</p></section>
<section class="sst-sec" id="sec-q"><h2>Q&A</h2><p>Q. 片耳だけでも使えますか? A. はい、左右独立で使えます。</p><p>Q. マルチポイント対応? A. 2台同時接続に対応します。</p></section>
</div>
</div>
CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.sst-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #fbfbfd; }
.sst-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; }
.sst-intro { padding: 22px 26px 14px; }
.sst-cat { margin: 0 0 6px; font-size: 11px; letter-spacing: .22em; font-weight: 700; color: #4f46e5; }
.sst-intro h1 { margin: 0; font-size: 24px; font-weight: 800; color: #1f2433; }
/* 上部に貼り付くタブ */
.sst-tabs { position: sticky; top: 0; z-index: 5; display: flex; gap: 2px; padding: 0 18px; background: rgba(251,251,253,.92); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid #e8ebf3; }
.sst-tab { position: relative; font: inherit; font-size: 13.5px; font-weight: 600; color: #6b7180; background: none; border: none; cursor: pointer; padding: 13px 14px; transition: color .2s ease; }
.sst-tab:hover { color: #3a4060; }
.sst-tab.is-active { color: #4f46e5; }
.sst-ink { position: absolute; bottom: -1px; left: 0; width: 0; height: 2.5px; border-radius: 3px; background: #4f46e5; transition: left .3s cubic-bezier(.2,.8,.2,1), width .3s cubic-bezier(.2,.8,.2,1); }
.sst-sec { padding: 18px 26px; min-height: 150px; color: #3a4050; line-height: 1.85; border-bottom: 1px solid #eef0f6; }
.sst-sec:last-child { min-height: 220px; }
.sst-sec h2 { margin: 0 0 10px; font-size: 18px; font-weight: 800; color: #1f2433; }
.sst-sec p { margin: 0 0 12px; font-size: 14px; max-width: 520px; }
@media (prefers-reduced-motion: reduce) { .sst-ink, .sst-tab { transition: none; } }
JavaScript
// 貼り付くタブ:現在のセクションをハイライトし下線を移動。クリックで移動
(() => {
const sc = document.getElementById('sstScroll');
const tabsWrap = document.getElementById('sstTabs');
const ink = document.getElementById('sstInk');
if (!sc || !tabsWrap || !ink) return;
const tabs = [...tabsWrap.querySelectorAll('.sst-tab')];
const secs = tabs.map(t => sc.querySelector('#' + t.dataset.to)).filter(Boolean);
const TABS_H = 46;
function moveInk(tab) { ink.style.left = tab.offsetLeft + 'px'; ink.style.width = tab.offsetWidth + 'px'; }
function setActive(idx) { tabs.forEach((t, i) => t.classList.toggle('is-active', i === idx)); moveInk(tabs[idx]); }
tabs.forEach((t, i) => t.addEventListener('click', () => {
auto = false;
if (secs[i]) sc.scrollTo({ top: secs[i].offsetTop - TABS_H, behavior: 'smooth' });
}));
function spy() {
const pos = sc.scrollTop + TABS_H + 10;
let idx = 0;
secs.forEach((s, i) => { if (s.offsetTop <= pos) idx = i; });
setActive(idx);
}
let ticking = false;
sc.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { spy(); ticking = false; }); }, { passive: true });
requestAnimationFrame(() => setActive(0));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches, dir = 1;
['wheel', 'touchstart', 'pointerdown'].forEach(ev => sc.addEventListener(ev, () => { auto = false; }, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = sc.scrollHeight - sc.clientHeight;
sc.scrollTop += dir * 1.5;
if (sc.scrollTop >= max - 1) dir = -1; else if (sc.scrollTop <= 0) dir = 1;
requestAnimationFrame(step);
}, 1100);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「追従セクションタブ」の効果を追加してください。
# 追加してほしい効果
追従セクションタブ(追従ウィジェット)
ページ内セクションへのタブが、スクロールで上部に貼り付き、表示中の区画をハイライトします。商品詳細の「概要/仕様/レビュー」のような長いページの章内ナビとして定番です。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- スクロールで上部に貼り付くセクションタブ(現在地ハイライト) -->
<div class="sst-frame">
<div class="sst-scroll" id="sstScroll">
<div class="sst-intro">
<p class="sst-cat">PRODUCT</p>
<h1>ワイヤレスイヤホン X</h1>
</div>
<div class="sst-tabs" id="sstTabs">
<button class="sst-tab is-active" type="button" data-to="sec-o">概要</button>
<button class="sst-tab" type="button" data-to="sec-s">仕様</button>
<button class="sst-tab" type="button" data-to="sec-r">レビュー</button>
<button class="sst-tab" type="button" data-to="sec-q">Q&A</button>
<span class="sst-ink" id="sstInk" aria-hidden="true"></span>
</div>
<section class="sst-sec" id="sec-o"><h2>概要</h2><p>軽量設計と高音質を両立したフラッグシップ。タブは上部に貼り付き、読んでいる区画をハイライトします。</p><p>長い商品ページの章内ナビとして定番の挙動です。</p></section>
<section class="sst-sec" id="sec-s"><h2>仕様</h2><p>連続再生8時間、ケース併用で32時間。Bluetooth 5.3、IPX4防滴対応。</p><p>対応コーデック:SBC / AAC / LDAC。</p></section>
<section class="sst-sec" id="sec-r"><h2>レビュー</h2><p>★4.7(1,204件)。「装着感が軽い」「通話がクリア」と高評価です。</p><p>低音の量感と解像度のバランスが好評です。</p></section>
<section class="sst-sec" id="sec-q"><h2>Q&A</h2><p>Q. 片耳だけでも使えますか? A. はい、左右独立で使えます。</p><p>Q. マルチポイント対応? A. 2台同時接続に対応します。</p></section>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.sst-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #fbfbfd; }
.sst-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; }
.sst-intro { padding: 22px 26px 14px; }
.sst-cat { margin: 0 0 6px; font-size: 11px; letter-spacing: .22em; font-weight: 700; color: #4f46e5; }
.sst-intro h1 { margin: 0; font-size: 24px; font-weight: 800; color: #1f2433; }
/* 上部に貼り付くタブ */
.sst-tabs { position: sticky; top: 0; z-index: 5; display: flex; gap: 2px; padding: 0 18px; background: rgba(251,251,253,.92); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid #e8ebf3; }
.sst-tab { position: relative; font: inherit; font-size: 13.5px; font-weight: 600; color: #6b7180; background: none; border: none; cursor: pointer; padding: 13px 14px; transition: color .2s ease; }
.sst-tab:hover { color: #3a4060; }
.sst-tab.is-active { color: #4f46e5; }
.sst-ink { position: absolute; bottom: -1px; left: 0; width: 0; height: 2.5px; border-radius: 3px; background: #4f46e5; transition: left .3s cubic-bezier(.2,.8,.2,1), width .3s cubic-bezier(.2,.8,.2,1); }
.sst-sec { padding: 18px 26px; min-height: 150px; color: #3a4050; line-height: 1.85; border-bottom: 1px solid #eef0f6; }
.sst-sec:last-child { min-height: 220px; }
.sst-sec h2 { margin: 0 0 10px; font-size: 18px; font-weight: 800; color: #1f2433; }
.sst-sec p { margin: 0 0 12px; font-size: 14px; max-width: 520px; }
@media (prefers-reduced-motion: reduce) { .sst-ink, .sst-tab { transition: none; } }
【JavaScript】
// 貼り付くタブ:現在のセクションをハイライトし下線を移動。クリックで移動
(() => {
const sc = document.getElementById('sstScroll');
const tabsWrap = document.getElementById('sstTabs');
const ink = document.getElementById('sstInk');
if (!sc || !tabsWrap || !ink) return;
const tabs = [...tabsWrap.querySelectorAll('.sst-tab')];
const secs = tabs.map(t => sc.querySelector('#' + t.dataset.to)).filter(Boolean);
const TABS_H = 46;
function moveInk(tab) { ink.style.left = tab.offsetLeft + 'px'; ink.style.width = tab.offsetWidth + 'px'; }
function setActive(idx) { tabs.forEach((t, i) => t.classList.toggle('is-active', i === idx)); moveInk(tabs[idx]); }
tabs.forEach((t, i) => t.addEventListener('click', () => {
auto = false;
if (secs[i]) sc.scrollTo({ top: secs[i].offsetTop - TABS_H, behavior: 'smooth' });
}));
function spy() {
const pos = sc.scrollTop + TABS_H + 10;
let idx = 0;
secs.forEach((s, i) => { if (s.offsetTop <= pos) idx = i; });
setActive(idx);
}
let ticking = false;
sc.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { spy(); ticking = false; }); }, { passive: true });
requestAnimationFrame(() => setActive(0));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches, dir = 1;
['wheel', 'touchstart', 'pointerdown'].forEach(ev => sc.addEventListener(ev, () => { auto = false; }, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = sc.scrollHeight - sc.clientHeight;
sc.scrollTop += dir * 1.5;
if (sc.scrollTop >= max - 1) dir = -1; else if (sc.scrollTop <= 0) dir = 1;
requestAnimationFrame(step);
}, 1100);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。