CSSで、サイドバーを途中から画面に追従させる方法
サイドバーのプロフィールなどはスクロールで流し、下にある目次だけを途中から画面に残すには、目次に position: sticky と top を付けます。JavaScript は要りません。ただし、サイドバーの列が本文と同じ高さまで伸びていないと、目次は追従しません。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。幅800px・高さ600pxの画面で検証用のページを開き、要素の位置を getBoundingClientRect() で測りました。スクロールは window.scrollTo() と Playwright のホイール操作で行っています。Firefox と Safari、スマートフォンの実機でのタッチのスクロールは試していません。

サイドバーの下の部品に sticky を付ける
HTML は、本文の main とサイドバーの aside を2列に並べたものです。サイドバーには、プロフィール、カテゴリー、目次の3つの部品を上から順に入れ、追従させたい目次にだけ sidebar-sticky というクラスを付けています。
<header class="site-header">それなら猫の手で</header>
<div class="layout">
<main>
<h2 id="s1">1. 見出し 1</h2>
<p>本文の段落 1-1。……</p>
<!-- 見出しと段落が続く -->
</main>
<aside class="sidebar">
<section class="widget profile">……</section>
<section class="widget categories">……</section>
<nav class="widget sidebar-sticky" aria-label="目次">
<h3>目次</h3>
<ul>
<li><a href="#s1">1. 見出し 1</a></li>
<!-- 2〜6 も同じ形 -->
</ul>
</nav>
</aside>
</div>
<footer class="site-footer">フッター</footer>
追従に関わる CSS は次のとおりです。枠線や文字の大きさなど、見た目だけの指定は省いています。
/* ページの上に固定するヘッダー(高さ56px) */
.site-header {
position: sticky;
top: 0;
z-index: 10;
height: 56px;
background: #fff;
border-bottom: 1px solid #d6eef3;
}
/* 本文とサイドバーの2列。サイドバーの列は本文と同じ高さまで伸びる */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 220px;
gap: 24px;
max-width: 760px;
margin: 0 auto;
padding: 16px;
}
/* サイドバーの中の、追従させたい部品 */
.sidebar-sticky {
position: sticky;
top: 72px; /* ヘッダーの高さ56px + すき間16px */
box-sizing: border-box;
max-height: calc(100vh - 88px); /* 上の72px + 下のすき間16px */
overflow-y: auto;
}
/* 1列にする幅では、追従させない */
@media (max-width: 767px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.sidebar-sticky {
position: static;
max-height: none;
overflow-y: visible;
}
}
top には、画面の上から何pxの位置で止めるかを書きます。この例ではヘッダーも画面の上に固定しているので、ヘッダーの高さ56pxにすき間の16pxを足した72pxにしています。
ページをスクロールした量(scrollY)ごとに、画面の上端から目次の上端までの距離を測りました。
scrollY |
目次の上端 |
|---|---|
| 0 | 465px |
| 200 | 265px |
| 392 | 73px |
| 393 | 72px |
| 800 | 72px |
| 1400 | 72px |
| 2488 | 72px |
| 2588 | -28px |
| 2674(いちばん下) | -114px |
最初の目次の上端は、ページの上から465pxのところにあります。そこから72pxを引いた393pxまでは、目次はプロフィールなどと一緒に上へ流れ、393pxで72pxの位置に着くと、そこで止まりました。これが「途中から追従する」動きです。MDN の position の説明のとおり、sticky の要素は、ふだんは文書の流れのとおりに置かれ、top などで決めた位置に来たところで止まります。
scrollY が2488pxを過ぎると、目次はまた上へ動き始めました。いちばん下までスクロールしたときは、目次の下端とサイドバーの列の下端が、どちらも画面の上から84pxの位置でそろっていました。MDN の同じページにあるとおり、sticky の要素が止まっていられるのは、要素の下端が包含ブロック(ここではサイドバーの列)の下端に着くまでです。そのため、本文が終わってフッターが見えてくると、目次はフッターに重ならずに、列の終わりで押し出されます。
追従しないときは、サイドバーの列の高さを見る
目次に sticky を付けても追従しないときは、サイドバーの列(aside)の高さを確かめます。親の .layout に align-items: start を足すと、目次は止まらなくなりました。
.layout の書き方 |
サイドバーの列の高さ | scrollY 400 |
800 | 1400 |
|---|---|---|---|---|
| グリッド(記事の CSS) | 2685px | 72px | 72px | 72px |
グリッド+align-items: start |
590px | 65px | -335px | -935px |
| フレックスボックス | 2685px | 72px | 72px | 72px |
フレックスボックス+align-items: flex-start |
590px | 65px | -335px | -935px |
数字は、画面の上端から目次の上端までの距離です。記事の CSS では、サイドバーの列の高さは本文(2685px)と同じでした。align-items: start を足すと、列の高さは中身の3つの部品の分(590px)だけになりました。列の終わりと目次の終わりが同じ位置になるので、目次が止まっていられる余地がなく、スクロールした分だけそのまま上へ流れていきます。

グリッドでもフレックスボックスでも、子の要素は何も書かなければ親と同じ高さまで伸びます。MDN の align-items の説明によると、初期値の normal は、フレックスボックスの子では stretch として、グリッドの子では stretch に近い形で働きます。サイドバーの列の高さがそろわないときは、親の align-items や、列の align-self に start などが書かれていないかを、ブラウザの開発者ツールで見直します。
フレックスボックスで2列にした場合も、結果はグリッドと同じでした。ほかに、ページ全体を囲む要素に overflow-x: hidden が付いている場合も、sticky の要素は止まりません。この場合の動きは、「Reactで、スクロールすると上に固定されるヘッダーを作る方法」の記事で確かめています。
サイドバー全体を追従させるときは、逆に伸ばさない
目次だけでなく、サイドバー全体を追従させたいときは、サイドバーの列(.sidebar)そのものに sticky を付けます。このときは、列が本文と同じ高さまで伸びていると追従しません。
.sidebar の CSS |
サイドバーの高さ | scrollY 400 |
800 | 1400 |
|---|---|---|---|---|
position: sticky; top: 72px; |
2685px | -327px | -727px | -1327px |
上に align-self: start; を足す |
590px | 72px | 72px | 72px |
数字は、画面の上端からサイドバーの上端までの距離です。列が伸びていると、サイドバーは親の .layout いっぱいの高さになり、親の中で動ける余地がありません。align-self: start を足して中身の高さに縮めると、72pxの位置で止まりました。
目次だけを追従させるときは列を伸ばし、サイドバー全体を追従させるときは列を縮める、と覚えておくと迷いません。なお、サイドバー全体を止める場合は、サイドバーの高さが画面に収まっている必要があります。この例では、scrollY が800pxのとき、サイドバーの下端は画面の上から662pxの位置にあり、高さ600pxの画面からはみ出していました。
目次が画面より長いときは max-height で区切る
目次の項目が多く、画面より長くなると、sticky で止めた目次の下のほうが画面の外に出たままになります。記事の CSS では、max-height を画面の高さから88px(上の72pxと下のすき間16px)を引いた値にして、はみ出した分は overflow-y: auto で目次の中をスクロールできるようにしています。
目次の項目を30個に増やし、scrollY が1000pxのときに測りました。画面の高さは600pxです。
| CSS | 目次の上端 | 目次の下端 | 30個目の項目の下端 |
|---|---|---|---|
| 記事の CSS | 72px | 584px | 829px |
max-height と overflow-y を外す |
72px | 846px | 829px |
記事の CSS では、目次の下端は584pxで、画面の中に収まりました。中身の高さは772pxあり、見えている部分は510pxです。目次の上にマウスカーソルを置いてホイールを下へ回すと、ページの scrollY は1000pxのまま、目次の中だけが262pxスクロールし、30個目の項目が567pxの位置に見えました。max-height と overflow-y を外すと、目次の下端は846pxで、30個目の項目は画面の外のままでした。
max-height の計算に、目次の枠線と上下の余白も含めるために、box-sizing: border-box を付けています。MDN の box-sizing の説明のとおり、border-box にすると、高さの指定にパディングと枠線が含まれます。100vh は、MDN の長さの単位の説明によると、ビューポートの高さの100%です。
1列にする幅では、追従と高さの上限を外す
幅が767px以下のときは、メディアクエリで1列にし、サイドバーを本文の下に回しています。幅360px・高さ640pxの画面で、目次の項目を30個にして測りました。
メディアクエリの中の .sidebar-sticky |
position |
目次の高さ | 100pxずつスクロールしたときの目次の上端 |
|---|---|---|---|
| 記事の CSS | static |
774px | 200px → 100px → 0px |
| 指定を書かない | sticky |
552px | 200px → 100px → 0px |
どちらも、100pxずつスクロールすると目次の上端は100pxずつ上がり、追従はしませんでした。1列のときはサイドバーの列が中身と同じ高さになるので、sticky が残っていても止まる余地がありません。
違いは目次の高さです。メディアクエリで指定を外さないと、max-height が残り、目次の高さは552pxで区切られて、収まらない項目は目次の中をスクロールして読む形になりました。本文の下に置いた目次は、ページと一緒にスクロールして読めば足ります。記事の CSS では、メディアクエリの中で position: static、max-height: none、overflow-y: visible を指定し、この3つをまとめて元に戻しています。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べた、最初に対応したバージョンです。
| 機能 | Chrome | Firefox | Safari | iOS の Safari |
|---|---|---|---|---|
position: sticky |
56 | 32 | 13(-webkit- 付きは7) |
13(-webkit- 付きは7) |
grid-template-columns |
57 | 52 | 10.1 | 10.3 |
グリッドでの align-self |
57 | 52 | 10.1 | 10.3 |





