スティッキー目次サイドバー
本文の隣に置いた目次サイドバーが position:sticky で追従し、スクロールしても画面内に留まり続ける。長文記事や技術ドキュメントの目次に最適な構成。
ライブデモ
コード
HTML
<!-- スティッキー目次サイドバー:本文と同じスクロールコンテナに aside を置き position:sticky で留める -->
<div class="ssd" id="ssdRoot">
<div class="ssd__frame">
<div class="ssd__scroll" id="ssdScroll">
<div class="ssd__row">
<article class="ssd__article">
<p class="ssd__eyebrow">スタイルガイド</p>
<h1>デザインシステム運用ガイド</h1>
<section id="sec1">
<h2>1. はじめに</h2>
<p>このガイドでは、複数のプロダクトチームで一貫したUIを保つための運用ルールをまとめています。トークン・コンポーネント・パターンの3層構造で管理します。</p>
</section>
<section id="sec2">
<h2>2. デザイントークン</h2>
<p>色・余白・角丸・影などの基礎値はすべてトークン化し、コード内での直接指定を避けます。トークンの変更はデザインレビューを経てから反映します。</p>
</section>
<section id="sec3">
<h2>3. コンポーネント設計</h2>
<p>ボタンやカードなどの共通部品は hover / focus / disabled の状態を必ず定義し、単独のコンポーネントとして独立にテストできる形にします。</p>
</section>
<section id="sec4">
<h2>4. アクセシビリティ</h2>
<p>コントラスト比・キーボード操作・フォーカスリングの3点は全コンポーネントの必須要件とし、リリース前チェックリストに組み込みます。</p>
</section>
<section id="sec5">
<h2>5. 運用フロー</h2>
<p>新規コンポーネントの追加は提案→レビュー→実装→ドキュメント化の順で進め、変更履歴はすべて Changelog に残します。</p>
</section>
<section id="sec6">
<h2>6. まとめ</h2>
<p>ここまでの内容を踏まえ、まずは自チームのボタンとカードから既存トークンへの置き換えを始めてみてください。</p>
</section>
</article>
<aside class="ssd__toc" aria-label="目次">
<p class="ssd__toc-label">目次</p>
<nav>
<a href="#sec1">1. はじめに</a>
<a href="#sec2">2. デザイントークン</a>
<a href="#sec3">3. コンポーネント設計</a>
<a href="#sec4">4. アクセシビリティ</a>
<a href="#sec5">5. 運用フロー</a>
<a href="#sec6">6. まとめ</a>
</nav>
</aside>
</div>
</div>
</div>
</div>
CSS
/* 全体:ドキュメントサイト風。本文と目次を同じスクロールコンテナに入れることで sticky を効かせる */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Hiragino Sans", system-ui, sans-serif;
background: linear-gradient(160deg, #eef2f8, #e4e9f2);
color: #1b2233;
display: flex; align-items: center; justify-content: center;
min-height: 100vh; padding: 20px;
}
/* ルート:width:100% が無いと親の flex 中央寄せで潰れるため必須 */
.ssd { width: 100%; display: flex; justify-content: center; }
.ssd__frame {
width: min(760px, 100%);
height: min(540px, 86vh);
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 24px 60px -30px rgba(15, 23, 42, .35);
}
/* スクロールコンテナ:本文とtocが同じ中にあるので aside の sticky が正しく効く */
.ssd__scroll {
height: 100%;
overflow-y: auto;
scroll-behavior: smooth;
padding: 30px 26px;
}
.ssd__scroll::-webkit-scrollbar { width: 8px; }
.ssd__scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
.ssd__row { display: flex; align-items: flex-start; gap: 26px; }
.ssd__article { flex: 1; min-width: 0; }
.ssd__eyebrow { font-size: 11px; font-weight: 800; letter-spacing: .22em; color: #6366f1; margin-bottom: 6px; }
.ssd__article h1 { font-size: clamp(19px, 3vw, 24px); margin-bottom: 18px; color: #12172a; }
.ssd__article section { margin-bottom: 22px; }
.ssd__article h2 { font-size: 15px; margin-bottom: 6px; color: #1b2233; }
.ssd__article p { font-size: 13.5px; line-height: 1.85; color: #4b5468; max-width: 46ch; }
/* 目次:position:sticky で追従。本文より背が低いので、スクロールすると途中で止まって見える */
.ssd__toc {
flex: 0 0 168px;
position: sticky;
top: 6px;
background: #f8fafc;
border: 1px solid #e6eaf2;
border-radius: 12px;
padding: 14px 12px;
}
.ssd__toc-label { font-size: 11px; font-weight: 800; color: #7c869c; letter-spacing: .08em; margin-bottom: 8px; }
.ssd__toc nav { display: flex; flex-direction: column; gap: 2px; }
.ssd__toc a {
display: block; font-size: 12.5px; color: #5b6478; text-decoration: none; padding: 6px 8px;
border-radius: 7px; transition: background .15s, color .15s;
}
.ssd__toc a:hover { background: #eef2ff; color: #4338ca; }
.ssd__toc a.is-active { background: #e0e7ff; color: #4338ca; font-weight: 700; }
/* かなり狭い時だけ縦積みに切り替え(560px幅程度までは横並びのまま sticky を実演できる) */
@media (max-width: 480px) {
.ssd__row { flex-direction: column; }
.ssd__toc { position: static; flex: 1 1 auto; width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
.ssd__scroll { scroll-behavior: auto; }
.ssd__toc a { transition: none; }
}
JavaScript
// 目次クリックでスクロール/スクロール位置に応じて目次をハイライト/
// 読み込み時に一度だけ自動スクロールし「本文は流れ目次は留まる」挙動を実演(null安全)
(() => {
const scroller = document.getElementById('ssdScroll');
if (!scroller) return;
const tocLinks = Array.from(document.querySelectorAll('.ssd__toc a'));
const sections = Array.from(document.querySelectorAll('.ssd__article section'));
if (!tocLinks.length || !sections.length) return;
const scrollToSection = (target) => {
if (!target) return;
const delta = target.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
scroller.scrollTo({ top: scroller.scrollTop + delta - 10, behavior: 'smooth' });
};
tocLinks.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const id = (link.getAttribute('href') || '').slice(1);
scrollToSection(document.getElementById(id));
});
});
const updateActive = () => {
const scrollerTop = scroller.getBoundingClientRect().top;
let current = sections[0];
sections.forEach((sec) => {
if (sec.getBoundingClientRect().top - scrollerTop <= 30) current = sec;
});
tocLinks.forEach((l) => l.classList.remove('is-active'));
const active = tocLinks.find((l) => l.getAttribute('href') === '#' + current.id);
if (active) active.classList.add('is-active');
};
scroller.addEventListener('scroll', updateActive, { passive: true });
updateActive();
// 読み込み時に一度だけ軽くスクロールし、本文が流れても目次が留まる様子を見せる
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduceMotion) {
setTimeout(() => scrollToSection(document.getElementById('sec2')), 700);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スティッキー目次サイドバー」の効果を追加してください。
# 追加してほしい効果
スティッキー目次サイドバー(レイアウト & グリッド)
本文の隣に置いた目次サイドバーが position:sticky で追従し、スクロールしても画面内に留まり続ける。長文記事や技術ドキュメントの目次に最適な構成。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- スティッキー目次サイドバー:本文と同じスクロールコンテナに aside を置き position:sticky で留める -->
<div class="ssd" id="ssdRoot">
<div class="ssd__frame">
<div class="ssd__scroll" id="ssdScroll">
<div class="ssd__row">
<article class="ssd__article">
<p class="ssd__eyebrow">スタイルガイド</p>
<h1>デザインシステム運用ガイド</h1>
<section id="sec1">
<h2>1. はじめに</h2>
<p>このガイドでは、複数のプロダクトチームで一貫したUIを保つための運用ルールをまとめています。トークン・コンポーネント・パターンの3層構造で管理します。</p>
</section>
<section id="sec2">
<h2>2. デザイントークン</h2>
<p>色・余白・角丸・影などの基礎値はすべてトークン化し、コード内での直接指定を避けます。トークンの変更はデザインレビューを経てから反映します。</p>
</section>
<section id="sec3">
<h2>3. コンポーネント設計</h2>
<p>ボタンやカードなどの共通部品は hover / focus / disabled の状態を必ず定義し、単独のコンポーネントとして独立にテストできる形にします。</p>
</section>
<section id="sec4">
<h2>4. アクセシビリティ</h2>
<p>コントラスト比・キーボード操作・フォーカスリングの3点は全コンポーネントの必須要件とし、リリース前チェックリストに組み込みます。</p>
</section>
<section id="sec5">
<h2>5. 運用フロー</h2>
<p>新規コンポーネントの追加は提案→レビュー→実装→ドキュメント化の順で進め、変更履歴はすべて Changelog に残します。</p>
</section>
<section id="sec6">
<h2>6. まとめ</h2>
<p>ここまでの内容を踏まえ、まずは自チームのボタンとカードから既存トークンへの置き換えを始めてみてください。</p>
</section>
</article>
<aside class="ssd__toc" aria-label="目次">
<p class="ssd__toc-label">目次</p>
<nav>
<a href="#sec1">1. はじめに</a>
<a href="#sec2">2. デザイントークン</a>
<a href="#sec3">3. コンポーネント設計</a>
<a href="#sec4">4. アクセシビリティ</a>
<a href="#sec5">5. 運用フロー</a>
<a href="#sec6">6. まとめ</a>
</nav>
</aside>
</div>
</div>
</div>
</div>
【CSS】
/* 全体:ドキュメントサイト風。本文と目次を同じスクロールコンテナに入れることで sticky を効かせる */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Hiragino Sans", system-ui, sans-serif;
background: linear-gradient(160deg, #eef2f8, #e4e9f2);
color: #1b2233;
display: flex; align-items: center; justify-content: center;
min-height: 100vh; padding: 20px;
}
/* ルート:width:100% が無いと親の flex 中央寄せで潰れるため必須 */
.ssd { width: 100%; display: flex; justify-content: center; }
.ssd__frame {
width: min(760px, 100%);
height: min(540px, 86vh);
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 24px 60px -30px rgba(15, 23, 42, .35);
}
/* スクロールコンテナ:本文とtocが同じ中にあるので aside の sticky が正しく効く */
.ssd__scroll {
height: 100%;
overflow-y: auto;
scroll-behavior: smooth;
padding: 30px 26px;
}
.ssd__scroll::-webkit-scrollbar { width: 8px; }
.ssd__scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
.ssd__row { display: flex; align-items: flex-start; gap: 26px; }
.ssd__article { flex: 1; min-width: 0; }
.ssd__eyebrow { font-size: 11px; font-weight: 800; letter-spacing: .22em; color: #6366f1; margin-bottom: 6px; }
.ssd__article h1 { font-size: clamp(19px, 3vw, 24px); margin-bottom: 18px; color: #12172a; }
.ssd__article section { margin-bottom: 22px; }
.ssd__article h2 { font-size: 15px; margin-bottom: 6px; color: #1b2233; }
.ssd__article p { font-size: 13.5px; line-height: 1.85; color: #4b5468; max-width: 46ch; }
/* 目次:position:sticky で追従。本文より背が低いので、スクロールすると途中で止まって見える */
.ssd__toc {
flex: 0 0 168px;
position: sticky;
top: 6px;
background: #f8fafc;
border: 1px solid #e6eaf2;
border-radius: 12px;
padding: 14px 12px;
}
.ssd__toc-label { font-size: 11px; font-weight: 800; color: #7c869c; letter-spacing: .08em; margin-bottom: 8px; }
.ssd__toc nav { display: flex; flex-direction: column; gap: 2px; }
.ssd__toc a {
display: block; font-size: 12.5px; color: #5b6478; text-decoration: none; padding: 6px 8px;
border-radius: 7px; transition: background .15s, color .15s;
}
.ssd__toc a:hover { background: #eef2ff; color: #4338ca; }
.ssd__toc a.is-active { background: #e0e7ff; color: #4338ca; font-weight: 700; }
/* かなり狭い時だけ縦積みに切り替え(560px幅程度までは横並びのまま sticky を実演できる) */
@media (max-width: 480px) {
.ssd__row { flex-direction: column; }
.ssd__toc { position: static; flex: 1 1 auto; width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
.ssd__scroll { scroll-behavior: auto; }
.ssd__toc a { transition: none; }
}
【JavaScript】
// 目次クリックでスクロール/スクロール位置に応じて目次をハイライト/
// 読み込み時に一度だけ自動スクロールし「本文は流れ目次は留まる」挙動を実演(null安全)
(() => {
const scroller = document.getElementById('ssdScroll');
if (!scroller) return;
const tocLinks = Array.from(document.querySelectorAll('.ssd__toc a'));
const sections = Array.from(document.querySelectorAll('.ssd__article section'));
if (!tocLinks.length || !sections.length) return;
const scrollToSection = (target) => {
if (!target) return;
const delta = target.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
scroller.scrollTo({ top: scroller.scrollTop + delta - 10, behavior: 'smooth' });
};
tocLinks.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const id = (link.getAttribute('href') || '').slice(1);
scrollToSection(document.getElementById(id));
});
});
const updateActive = () => {
const scrollerTop = scroller.getBoundingClientRect().top;
let current = sections[0];
sections.forEach((sec) => {
if (sec.getBoundingClientRect().top - scrollerTop <= 30) current = sec;
});
tocLinks.forEach((l) => l.classList.remove('is-active'));
const active = tocLinks.find((l) => l.getAttribute('href') === '#' + current.id);
if (active) active.classList.add('is-active');
};
scroller.addEventListener('scroll', updateActive, { passive: true });
updateActive();
// 読み込み時に一度だけ軽くスクロールし、本文が流れても目次が留まる様子を見せる
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduceMotion) {
setTimeout(() => scrollToSection(document.getElementById('sec2')), 700);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。