CSSで、表の見出しを固定してスクロールする方法
縦に長い表をスクロールしても見出しの行を上に残すには、表を max-height と overflow: auto を付けた枠に入れ、見出しのセル(thead th)に position: sticky と top: 0 を付けます。JavaScript は要りません。同じ仕組みで左の列も固定でき、横に長い表をスマートフォンで見せるときにも使えます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。幅360pxの画面で検証用のページを開き、要素の位置を getBoundingClientRect() で測りました。Firefox と Safari、スマートフォンの実機は試していません。

表を枠に入れ、見出しのセルに sticky を付ける
HTML は、ふつうの表を div で囲んだだけです。見出しの行を <thead> に、各行の見出し(商品名)を <th scope="row"> にしています。
<div class="table-wrap">
<table>
<caption>月ごとの販売数(個)</caption>
<thead>
<tr><th scope="col">商品</th><th scope="col">4月</th><th scope="col">5月</th><!-- 6月〜9月も同じ形 --></tr>
</thead>
<tbody>
<tr><th scope="row">商品 1</th><td>47</td><td>10</td><!-- 6月〜9月も同じ形 --></tr>
<!-- 商品 2〜30 も同じ形 -->
</tbody>
</table>
</div>
CSS は次のとおりです。
/* 見出しの行と左の列を固定して、表の中だけをスクロールさせる */
.table-wrap {
max-height: 320px;
overflow: auto;
border: 1px solid #b8e6ef;
}
.table-wrap table {
min-width: 640px;
border-collapse: separate;
border-spacing: 0;
font-size: 14px;
}
.table-wrap th,
.table-wrap td {
padding: 8px 12px;
border-bottom: 1px solid #d6eef3;
white-space: nowrap;
text-align: left;
}
/* 見出しの行を上に固定する */
.table-wrap thead th {
position: sticky;
top: 0;
z-index: 1;
background: #eefafd;
border-bottom: 2px solid #00c8e8;
}
/* 左の列(各行の見出し)を左に固定する */
.table-wrap tbody th {
position: sticky;
left: 0;
background: #fff;
}
/* 左上のマスは、上にも左にも固定して、いちばん手前に出す */
.table-wrap thead th:first-child {
left: 0;
z-index: 2;
}
見出しの行を残しているのは、枠の max-height と overflow: auto、見出しのセルの position: sticky と top: 0 の組み合わせです。MDN の position の説明によると、sticky の要素は、スクロールの仕組みを持ついちばん近い祖先(overflow が hidden、scroll、auto などの要素)に対して止まります。この CSS では、それが .table-wrap です。
枠を縦にスクロールして、枠の上端から見出しのセル「4月」の上端までの距離を測りました。
scrollTop |
記事の CSS | sticky を付けない |
top を書かない |
|---|---|---|---|
| 0 | 20px | 20px | 20px |
| 100 | 0px | -80px | -80px |
| 400 | 0px | -380px | -380px |
最初の20pxは、見出しの上にある <caption> の高さです。記事の CSS では、表題がスクロールで隠れたあと、見出しの行は枠の上端(0px)に残りました。sticky を付けないと、見出しの行はほかの行と一緒に上へ流れます。
top を書き忘れたときも、sticky を付けていない場合と同じ数字になりました。MDN の同じページにあるとおり、止めたい向きの top や left などが auto のままだと、sticky は relative と同じ動きになります。sticky の基本的な動きは、スクロールすると上に固定されるヘッダーの記事でも確かめています。
sticky を付ける場所は、th のほかに thead や thead tr でも試しました。Chromium 153 では、どれも上の表の「記事の CSS」と同じ数字になりました。この記事では、左上のマスだけ別の値を付けたいので、セルごとに付けられる th にしています。
左の列も固定し、左上のマスを手前に出す
横に長い表では、各行の見出し(tbody th)に position: sticky と left: 0 を付けると、左の列が残ります。幅360pxの画面では枠の中の幅が326px、表の幅が640pxなので、表は横にもスクロールします。枠を横に150pxスクロールしても、「商品 3」のセルの左端は枠の左端(0px)のままでした。
左上のマス(見出しの行の「商品」)は、上にも左にも止める必要があります。thead th で top: 0 が付いているので、thead th:first-child には left: 0 を足します。
このとき、重なり順に気をつけます。縦に300px、横に150pxスクロールした状態で、左上のマスの位置に、いちばん手前に見えている要素を document.elementFromPoint() で調べました。
| CSS | 左上のマスの真ん中に見えるもの |
|---|---|
記事の CSS(左上のマスに z-index: 2) |
「商品」(左上のマス) |
左上のマスを、ほかの見出しと同じ z-index: 1 にする |
「5月」(見出しの行のセル) |
見出しの行の z-index をすべて外す |
「商品 8」(左の列のセル) |
左上のマスとほかの見出しのセルが同じ z-index だと、見出しの行の「5月」のセルが、横に流れてきて左上のマスの上に重なりました。見出しの行の z-index をすべて外すと、今度は表の本体(tbody)の左の列が、見出しの行の上に重なりました。MDN の z-index の説明のとおり、z-index が大きい要素ほど手前に出ます。見出しの行を左の列より手前に出すために 1、左上のマスをさらに手前に出すために 2 を付けています。
線が消える・文字が重なるときに見直すこと
見出しの行を止めたときにつまずきやすいのは、線と背景色です。枠を300pxスクロールした状態を、記事の CSS と、線や背景の書き方を変えた2つの場合で撮って比べました。

1つ目は、表の線をまとめる border-collapse: collapse です。見出しのセル「5月」の下端から1px上の色を測ると、スクロールする前はどちらも線の色(#00c8e8)でした。300pxスクロールしたあとは、separate では #00c8e8 のまま、collapse では白(#ffffff)になり、線は見出しの行と一緒に残りませんでした。線も一緒に残したいときは、記事の CSS のように border-collapse: separate と border-spacing: 0 にして、セルに border-bottom を付けます。MDN の互換性データにも、Firefox について、collapse の表では見出しの線が表示されないという注記があります。
2つ目は、見出しのセルの背景色です。sticky のセルは、下の行の上に重なって止まります。セルに背景色がないと、後ろを流れていく行の文字が透けて見えます。見出しの行には #eefafd、左の列には白の背景色を付けています。
ページ全体のスクロールで止めたいとき
枠に max-height を付けずに、ページ全体をスクロールしたときに見出しの行を画面の上端へ止めたい場合もあります。このときは、枠の overflow の付け方で結果が変わりました。
| 枠の CSS | 枠の計算値 | scrollY 0 |
300 | 700 |
|---|---|---|---|---|
overflow-x: auto だけ |
overflow-y も auto |
37px | -263px | -663px |
overflow を付けない |
overflow-y は visible |
37px | 0px | 0px |
数字は、画面の上端から見出しのセル「4月」の上端までの距離です。横にはみ出す表によく付ける overflow-x: auto だけを書いた場合、overflow-y は書いていないのに auto になりました。MDN の overflow の説明にあるとおり、片方の向きを visible のままにしても、もう片方が visible か clip 以外なら、visible の向きは auto として扱われます。すると枠がスクロールの仕組みを持つ祖先になります。MDN の position の説明によると、sticky の要素は、その祖先がスクロールしない場合でも、その祖先に対して止まります。この枠は clientHeight と scrollHeight がどちらも1168pxで、縦にはスクロールしません。そのため、ページをスクロールすると見出しの行も一緒に流れました。この仕組みは、スクロールすると上に固定されるヘッダーの記事でも、overflow-x: hidden の場合で確かめています。
枠に overflow を付けない場合は、見出しの行が画面の上端に止まりました。ただし、この場合は横にはみ出した表をスクロールする枠がありません。横に長い表で、見出しの行も左の列も止めたいときは、記事の CSS のように枠に max-height を付けて、枠の中で縦にも横にもスクロールさせます。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べた、最初に対応したバージョンです。
| 機能 | Chrome | Firefox | Safari | iOS の Safari |
|---|---|---|---|---|
position: sticky |
56 | 32 | 13(-webkit- 付きは7) |
13(-webkit- 付きは7) |
表の要素での sticky |
56 | 59 | 8 | 8 |





