症状:position: stickyを書いたのに固定されない
position: sticky を指定したのに、スクロールしても要素が上に貼り付かず、そのまま流れて消えてしまう。ヘッダーやテーブルの見出し行を追従させたいのに効かない。この記事は、その原因を切り分けて直すためのチェックリストです。
やっかいなのは、stickyは要素そのものではなく「親(祖先)の状態」で効かなくなる点です。指定した要素をいくら見直しても直りません。親をさかのぼって確認する必要があります。
効かない原因は次の4つ
実務で遭遇するstickyの不発は、ほぼ次の4パターンに収まります。上から順に確認してください。それぞれに失敗デモと修正デモ、DevToolsでの見つけ方を付けています。
原因1:top(またはbottom等)を指定していない
position: sticky だけでは効かない。どの位置で貼り付けるかの閾値(top: 0 など)が必須。
原因2:親(祖先)に overflow: hidden / auto / scroll が付いている
stickyは「一番近い、スクロール可能な祖先」を基準に動く。途中の親に overflow があると、その枠が新しい基準になって外側では効かなくなる。
原因3:sticky要素の親の高さが足りない
stickyは親要素の範囲内でしか粘着しない。親が中身とぴったり同じ高さだと、動ける余地がなく最初から貼り付かない。
原因4:flex / grid の子で高さいっぱいに引き伸ばされている
親が display: flex(既定は align-items: stretch)だと、sticky要素が親いっぱいの高さに伸びてしまい、原因3と同じく動く余地がなくなる。
「テーブルの thead を固定したいのに効かない」ケースもこの4つのどれか(多くは原因2の overflow)に含まれます。テーブル固有の注意点は記事後半でまとめます。
原因1:topを指定していない
一番多い勘違いです。position: sticky は「どの位置に来たら貼り付くか」を top / bottom / left / right のいずれかで指定して初めて機能します。閾値がないと、ブラウザは貼り付けるべき位置を判断できず、普通の要素として流れていきます。
下のデモは同じ青い見出しですが、左は top なし、右は top: 0 です。枠の中をスクロールして違いを確認してください。
枠内をスクロールすると見出しがそのまま流れて消えます。
見出しが枠の上端に貼り付いたままになります。
/* 効かない:閾値がない */
.head {
position: sticky;
}
/* 効く:貼り付ける位置を指定する */
.head {
position: sticky;
top: 0; /* 上端から 0px の位置で粘着する */
}
自分のケースか確認する方法:DevToolsで該当要素を選び、Stylesパネルで top / bottom のいずれかが指定されているか見ます。どれも付いていなければこの原因です。top: auto(初期値)のままでも効きません。
原因2:親(祖先)に overflow が付いている
stickyは「一番近い、スクロール可能な祖先」を基準に貼り付きます。ページ全体(ビューポート)を基準にしたいのに、途中の親に overflow: hidden / auto / scroll / clip が付いていると、その親が新しい基準になります。しかもその親自身がスクロールしないなら、stickyは一度も貼り付くタイミングがないまま流れます。
overflow: hidden は「はみ出しを隠すだけ」のつもりで付けがちですが、これがstickyを止める最頻出の犯人です。
下のデモは、外側のスクロール枠は同じで、中間ラッパーの overflow だけが違います。
見出しを含むラッパーに overflow: hidden が付いているため、見出しが貼り付きません。
余計な overflow を外し、見出しは直接スクロール枠の子にします。
/* 犯人:中間の親に overflow が付いている */
.wrapper {
overflow: hidden; /* これが sticky の基準を奪う */
}
/* 対策1:不要なら overflow を外す(visible に戻す) */
.wrapper {
overflow: visible;
}
/* 対策2:スクロールさせたい枠自体に sticky の親子関係を寄せる。
overflow を付ける枠と、sticky を効かせたい基準を一致させる */
自分のケースか確認する方法:sticky要素から親を1つずつさかのぼり、DevToolsのStylesパネルで各要素の overflow(overflow-x / overflow-y 含む)を確認します。visible 以外が見つかったら、それが基準を奪っている候補です。overflow: hidden を一時的に visible に切り替えてstickyが復活するかで犯人を特定できます。
「はみ出し対策で付けた overflow: hidden が、別の場所の sticky を静かに殺している」——これがこのプロパティで最も見落とされる関係です。
原因3:sticky要素の親の高さが足りない
stickyは「親要素の中」でしか粘着しません。親の下端に達すると、そこで一緒にスクロールアウトします。ここで問題になるのが、親が中身とぴったり同じ高さしかないケースです。親に動ける余地(余白の高さ)がないと、貼り付いた瞬間に親ごと画面外へ出てしまい、実質「効いていない」ように見えます。
下のデモは、外側のスクロール枠の中に「見出し+数行だけの短い親」と「その後ろに続くコンテンツ」を置いた構成です。左は見出しを短い親の中に入れているため、親を過ぎるとすぐ消えます。右は見出しをスクロール枠の直下に置き、十分な高さの中で粘着させています。
見出しの親が3行分しかないので、すぐに一緒に消えます。
見出しを高さのある親(スクロール枠の直下)に置くと、下端まで貼り付き続けます。
/* 症状:見出しの親が中身と同じ高さしかない
→ 粘着する余地がなく、親を過ぎると一緒に消える */
/* 対策:sticky 要素を「スクロールする範囲全体を覆う親」の
直接の子にする。親の高さ=スクロールする長さになるので
その間ずっと粘着する */
.scroll-area > .head {
position: sticky;
top: 0;
}
自分のケースか確認する方法:DevToolsで sticky要素の親を選び、Layout(またはボックスモデル)で親の高さを確認します。親の高さが sticky要素+わずかな中身分しかなければこの原因です。stickyは「親の高さ − 自分の高さ」の範囲でしか動けない、と覚えておくと切り分けが速くなります。
原因4:flex / gridの子で高さいっぱいに引き伸ばされている
サイドバーやテーブルの見出し列をstickyにしたいとき、親が display: flex や display: grid だと引っかかります。flexの子はデフォルトで align-items: stretch、つまり親いっぱいの高さに引き伸ばされます。sticky要素が親と同じ高さになると、原因3と同じく動く余地がなくなり貼り付きません。
下のデモは、左右にサイドバー(stickyにしたい)とコンテンツを並べたflexレイアウトです。左は align-items: stretch(既定)でサイドバーが引き伸ばされ、右は align-self: flex-start で自分の高さに戻しています。
左のラベルが親いっぱいに伸びているため貼り付きません。
(stretch)
左のラベルを自分の高さに戻すと、スクロール中も上端に貼り付きます。
(sticky)
/* 症状:flex の子はデフォルトで stretch = 親いっぱいの高さ
→ 動く余地がなく sticky が効かない */
.layout {
display: flex;
/* align-items: stretch(初期値)が効いている */
}
/* 対策:sticky にする子だけ引き伸ばしを打ち消す */
.sidebar {
position: sticky;
top: 0;
align-self: flex-start; /* stretch をやめて自分の高さに戻す */
}
自分のケースか確認する方法:DevToolsで sticky要素を選び、Computedパネルで align-self(またはgridの align-self)を確認します。値が stretch になっていて、要素の高さが親と一致しているならこの原因です。align-self: flex-start(gridなら start)を足して直します。
テーブルのthead / thを固定したいとき
「テーブルの見出し行を固定したいのに効かない」というのも、実体は上の4原因のどれかです。ただしテーブルには追加の注意点があります。
スクロールする枠は table の外側に付ける
横スクロールや縦スクロールのために overflow: auto を付ける枠は table を囲む div 側に置きます。その枠が sticky の基準になり、thead はその枠の上端に貼り付きます。table 自身に overflow を付けても意図どおりには動きません。
sticky は th に付けると確実
thead や tr への sticky はブラウザによって挙動差があります。各 th に position: sticky; top: 0; を付けるのが最も確実です。th は初期状態でも十分な高さの範囲(テーブル全体)を親に持つため、原因3にも当たりにくくなります。
<div class="table-scroll"> <!-- ここに overflow を付ける -->
<table>
<thead>
<tr><th>名前</th><th>部署</th><th>状態</th></tr>
</thead>
<tbody>...</tbody>
</table>
</div>
.table-scroll {
max-height: 400px;
overflow: auto; /* この枠が sticky の基準になる */
}
/* thead ではなく th に付けるのが確実 */
.table-scroll thead th {
position: sticky;
top: 0;
background: #fff; /* 背景を指定しないと下の行が透けて重なる */
}
背景色の指定を忘れると、貼り付いた見出しの下を行が通り抜けて文字が重なって見えます。stickyな見出しには不透明な background を必ず付けてください。動くサンプルは記事末尾のスティッキーヘッダー事例で確認できます。
直らないときの切り分け手順
4つのデモを見ても直らない場合は、次の順で機械的に確認します。原因を1つずつ潰していくのがstickyのデバッグの基本です。
手順1:topが付いているか
Stylesパネルで top / bottom のいずれかを確認(原因1)。
手順2:親をさかのぼって overflow を確認
sticky要素から body まで各親の overflow を見て、visible 以外を探す(原因2)。一時的に visible に変えて復活するかで犯人を確定する。
手順3:親の高さと自分の高さを比べる
親の高さが「sticky要素+少し」しかなければ、動く余地がない(原因3)。flexの stretch で伸びていないかも同時に見る(原因4)。
まとめ
position: stickyが効かないときは、指定した要素ではなく親(祖先)の状態を疑うのが近道です。確認する順番は次のとおりです。
topを指定しているか → 親に overflow が付いていないか → 親に動く余地(高さ)があるか → flex/gridで引き伸ばされていないか。
この4点を順に潰せば、実務で出会うstickyの不発はほぼ解消できます。テーブルの見出し固定は、スクロール枠を table の外に置き、th に sticky と背景色を付けるのが確実です。
同じシリーズの関連記事
「効かない・動かない」トラブル解決シリーズの記事です。症状から原因を切り分けたいときにあわせてどうぞ。