CSSで、サイドバーを途中から画面に追従させる方法

​​CSS​CSS Grid,Flexbox,HTML,Playwright

サイドバーのプロフィールなどはスクロールで流し、下にある目次だけを途中から画面に残すには、目次に 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、スマートフォンの実機でのタッチのスクロールは試していません。

本文の上でホイールを4回、下へ回す。1回目で、プロフィールとカテゴリーの部品は上へ流れ、その下の目次がヘッダーのすぐ下まで上がって止まる。2回目と3回目は、本文だけが流れ、目次は同じ位置に残る。4回目でフッターが見えてくると、目次は本文の終わりで上へ押し出される。左下に出している scrollY と目次の上端の数字は、値が変わっている間と止まった直後だけピンクになる。2回目と3回目は、目次の上端は72pxのままで、ピンクにならない
ホイールを4回回したときの目次。途中から画面の上に残り、本文の終わりで押し出される様子(スクロールは1回分を10コマに分けて動かしたもの。カーソル、ホイールの表示、回数、数字は GIF 用に重ねたもの)

サイドバーの下の部品に sticky を付ける

HTML は、本文の main とサイドバーの aside を2列に並べたものです。サイドバーには、プロフィール、カテゴリー、目次の3つの部品を上から順に入れ、追従させたい目次にだけ sidebar-sticky というクラスを付けています。

html
<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 は次のとおりです。枠線や文字の大きさなど、見た目だけの指定は省いています。

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)だけになりました。列の終わりと目次の終わりが同じ位置になるので、目次が止まっていられる余地がなく、スクロールした分だけそのまま上へ流れていきます。

上下2段の比較。どちらも scrollY が800pxのときの画面。上段の記事の CSS では、ヘッダーの下、本文の右に目次が見えている。下段の align-items: start では、本文だけが見えていて、右のサイドバーには何も残っていない
scrollY 800pxのときの画面。サイドバーの列が中身の高さで終わると、目次は残らない

グリッドでもフレックスボックスでも、子の要素は何も書かなければ親と同じ高さまで伸びます。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

​Posted by 86