position: stickyが効かない4つの原因と直し方(コピペで確認できるデモ付き)

症状: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 です。枠の中をスクロールして違いを確認してください。

効かないパターン(top なし)

枠内をスクロールすると見出しがそのまま流れて消えます。

追従させたい見出し(top 未指定)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
効くパターン(top: 0)

見出しが枠の上端に貼り付いたままになります。

追従させたい見出し(top: 0)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
/* 効かない:閾値がない */
.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: hidden が付いているため、見出しが貼り付きません。

見出し(親が overflow: hidden)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
効くパターン(中間ラッパーの overflow を外す)

余計な overflow を外し、見出しは直接スクロール枠の子にします。

見出し(余計な overflow なし)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
/* 犯人:中間の親に overflow が付いている */
.wrapper {
  overflow: hidden; /* これが sticky の基準を奪う */
}

/* 対策1:不要なら overflow を外す(visible に戻す) */
.wrapper {
  overflow: visible;
}

/* 対策2:スクロールさせたい枠自体に sticky の親子関係を寄せる。
   overflow を付ける枠と、sticky を効かせたい基準を一致させる */

自分のケースか確認する方法:sticky要素から親を1つずつさかのぼり、DevToolsのStylesパネルで各要素の overflowoverflow-x / overflow-y 含む)を確認します。visible 以外が見つかったら、それが基準を奪っている候補です。overflow: hidden を一時的に visible に切り替えてstickyが復活するかで犯人を特定できます。

「はみ出し対策で付けた overflow: hidden が、別の場所の sticky を静かに殺している」——これがこのプロパティで最も見落とされる関係です。

原因3:sticky要素の親の高さが足りない

stickyは「親要素の中」でしか粘着しません。親の下端に達すると、そこで一緒にスクロールアウトします。ここで問題になるのが、親が中身とぴったり同じ高さしかないケースです。親に動ける余地(余白の高さ)がないと、貼り付いた瞬間に親ごと画面外へ出てしまい、実質「効いていない」ように見えます。

下のデモは、外側のスクロール枠の中に「見出し+数行だけの短い親」と「その後ろに続くコンテンツ」を置いた構成です。左は見出しを短い親の中に入れているため、親を過ぎるとすぐ消えます。右は見出しをスクロール枠の直下に置き、十分な高さの中で粘着させています。

効かないパターン(親が短い)

見出しの親が3行分しかないので、すぐに一緒に消えます。

見出し(親は3行分だけ)
この親の行 1
この親の行 2
この親の行 3
— ここから下は別のコンテンツ —
続き 1
続き 2
続き 3
続き 4
続き 5
続き 6
効くパターン(親が十分に高い)

見出しを高さのある親(スクロール枠の直下)に置くと、下端まで貼り付き続けます。

見出し(親が十分に高い)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
/* 症状:見出しの親が中身と同じ高さしかない
   → 粘着する余地がなく、親を過ぎると一緒に消える */

/* 対策:sticky 要素を「スクロールする範囲全体を覆う親」の
   直接の子にする。親の高さ=スクロールする長さになるので
   その間ずっと粘着する */
.scroll-area > .head {
  position: sticky;
  top: 0;
}

自分のケースか確認する方法:DevToolsで sticky要素の親を選び、Layout(またはボックスモデル)で親の高さを確認します。親の高さが sticky要素+わずかな中身分しかなければこの原因です。stickyは「親の高さ − 自分の高さ」の範囲でしか動けない、と覚えておくと切り分けが速くなります。

原因4:flex / gridの子で高さいっぱいに引き伸ばされている

サイドバーやテーブルの見出し列をstickyにしたいとき、親が display: flexdisplay: grid だと引っかかります。flexの子はデフォルトで align-items: stretch、つまり親いっぱいの高さに引き伸ばされます。sticky要素が親と同じ高さになると、原因3と同じく動く余地がなくなり貼り付きません。

下のデモは、左右にサイドバー(stickyにしたい)とコンテンツを並べたflexレイアウトです。左は align-items: stretch(既定)でサイドバーが引き伸ばされ、右は align-self: flex-start で自分の高さに戻しています。

効かないパターン(stretch で引き伸ばし)

左のラベルが親いっぱいに伸びているため貼り付きません。

サイドバー
(stretch)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
効くパターン(align-self: flex-start)

左のラベルを自分の高さに戻すと、スクロール中も上端に貼り付きます。

サイドバー
(sticky)
行 1
行 2
行 3
行 4
行 5
行 6
行 7
行 8
行 9
行 10
/* 症状: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 に付けると確実

theadtr への sticky はブラウザによって挙動差があります。各 thposition: 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 の外に置き、thsticky と背景色を付けるのが確実です。

同じシリーズの関連記事

「効かない・動かない」トラブル解決シリーズの記事です。症状から原因を切り分けたいときにあわせてどうぞ。