position: sticky が効かない原因の多くは、「top などの指定がない」「祖先にスクロールコンテナがある」「動ける余白(親の高さ)がない」のどれかです。 sticky は「一番近いスクロールコンテナ」の中で、「親要素(包含ブロック)の範囲内」でだけ貼り付く仕組みなので、この2つの条件を確かめれば、ほとんどのケースは原因を特定できます。

この記事では、原因を 10パターン に分けて「症状 → 確認方法 → 修正CSS」の早見表にまとめました。さらに、パターンごとの最小再現 HTML を作り、Playwright+Chromium 141 でスクロールして getBoundingClientRect().top を測った結果を載せています。祖先の overflow を一発で洗い出すコンソール用 JS スニペット(実行結果つき)、overflow: clip で回避できるケースの実測、そのまま使える sticky ヘッダー・サイドバー・表見出しのコードも紹介します。


position: sticky が効かない原因の早見表

# 原因 症状 確認方法 修正CSS
1 top / bottom / left / right の指定がない 普通にスクロールで流れる Computed で top: auto top: 0; を追加
2 祖先に overflow: hidden / auto / scroll 流れる(ページ全体のスクロールでは止まらない) 下の JS スニペット overflow: clip に変える、または overflow を外す
3 親の高さが sticky 要素と同じ まったく動かない 親の高さ=子の高さ sticky を指定する要素を、長い親の直下に移す
4 flex の align-items: stretch サイドバーが流れる サイドバーの高さが親いっぱい align-self: flex-start
5 grid の stretch 同上 同上 align-self: start
6 表の外側に overflow-x: auto 見出し行が流れる テーブルの親要素の overflow 外側に max-height を付けて中でスクロール、または overflow-x: clip
7 親要素自体が短い(セクション内の sticky) 途中まで止まり、その後一緒に流れる 親の下端と一緒に動く 仕様どおり。止めたい範囲まで親を広げる
8 display: inline で親が短い 流れる 親(p など)の高さが1行分 display: block にして長い親に置く
9 z-index で後ろの要素に隠れる 止まっているのに見えない document.elementFromPoint z-index: 10 などを付ける
10 スクロールしているコンテナの取り違え(div・iframe) ページを動かしても止まらない どの要素の scrollTop が変わるか 内側のコンテナをスクロールさせる設計にする
参考 transform / filter / will-change / contain: paint Chromium 141 では影響なし(実測) — 原因候補から外してよい

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「position: stickyが効かない原因10選|チェックリストと修正コード付き」


position: sticky が貼り付く仕組み

sticky 要素は、通常は relative のように流れ、スクロールで指定位置(top: 0 など)に来ると fixed のように止まります。 ただし止まれる範囲には2つの制限があります。

制限 内容 外れるとどうなるか
どこを基準に止まるか 最も近い祖先のスクロールコンテナ(overflow が visible / clip 以外の要素)。なければビューポート 祖先に overflow: hidden などがあると、その要素の中で止まろうとする。その要素自体がスクロールしなければ、止まったようには見えない
どこまで止まれるか 親要素(包含ブロック)の内側だけ 親の下端が来ると、sticky 要素も押し出されて一緒に流れる
flowchart TD
    A[sticky が効かない] --> B{top などを指定した?}
    B -- いいえ --> B1[top: 0 を追加]
    B -- はい --> C{祖先に overflow が visible/clip 以外の要素がある?}
    C -- はい --> C1[overflow: clip に変える / その要素の中でスクロールさせる]
    C -- いいえ --> D{親要素は sticky 要素より十分に高い?}
    D -- いいえ --> D1[flex/grid なら align-self を変える / 長い親に移す]
    D -- はい --> E{止まっているのに見えない?}
    E -- はい --> E1[z-index を付ける]
    E -- いいえ --> F[スクロールしている要素を確認 / iframe 内か確認]

10パターンを Chromium で実測した結果

検証環境:Python Playwright 1.56.0+Chromium 141.0.7390.37(headless)、ビューポート 800×600。 各パターンの最小 HTML を読み込み、ページを 0px・200px・600px スクロールしたときの sticky 要素の getBoundingClientRect().top を測りました。0 のままなら貼り付いている、スクロール量に合わせて負になるなら流れているという見方です。

ケース 0px 200px 600px 判定
基準(top: 0 を指定) 0 0 0 固定された
1. top 等の指定なし 0 -200 -600 固定されない
2a. 祖先に overflow: hidden 0 -200 -600 固定されない
2b. 祖先に overflow: auto(高さ指定なし) 0 -200 -600 固定されない
2c. 祖先に overflow-x: hidden のみ 0 -200 -600 固定されない
2d. 祖先に overflow: clip 0 0 0 固定された
2e. 祖先に overflow-x: clip 0 0 0 固定された
2f. body だけに overflow-x: hidden 0 0 0 固定された
2g. html と body の両方に overflow-x: hidden 0 -200 -600 固定されない
3. 親の高さが要素と同じ 0 -200 -600 固定されない
4a. flex 既定(stretch) 0 -200 -600 固定されない
4b. flex+align-self: flex-start 0 0 0 固定された
5a. grid 既定(stretch) 0 -200 -600 固定されない
5b. grid+align-self: start 0 0 0 固定された
6a. table:th に sticky 2 0 0 固定された
6b. table:thead に sticky 2 0 0 固定された
6c. table:tr に sticky 2 0 0 固定された
6d. table:外側の div に overflow-x: auto 2 -198 -598 固定されない
7. 親要素自体が短い(高さ300pxのセクション内) 0 0 -340 途中まで固定
8a. display: inline の span(親は p) 14 -186 -586 固定されない
8b. inline だが親が長い 0 0 0 固定された
参考:祖先に transform: translateZ(0) 0 0 0 固定された
参考:祖先に contain: paint 0 0 0 固定された
参考:祖先に will-change: transform 0 0 0 固定された
参考:祖先に filter: blur(0) 0 0 0 固定された
参考:親が display: contents 0 0 0 固定された

表の 6a〜6d の「2」は table の既定の border-spacing(2px)の分です。測定に使った HTML の土台は次のとおりです。

<!doctype html>
<html><head><meta charset="utf-8"><style>
  * { box-sizing: border-box }
  body { margin: 0 }
  .s { background: #fc0; height: 40px }
  .tall { height: 3000px }
  /* ここに各ケースの CSS(例:.w { overflow: hidden } #t { position: sticky; top: 0 }) */
</style></head>
<body>
  <!-- 例:ケース 2a -->
  <div class="w"><div id="t" class="s">H</div><div class="tall"></div></div>
</body></html>
// 測定スクリプト(ページ内で実行)
const raf = () => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
const t = document.getElementById('t');
for (const y of [0, 200, 600]) {
  window.scrollTo(0, y);
  await raf();
  console.log(y, t.getBoundingClientRect().top);
}

原因ごとの解説と修正コード

原因1:top などの位置指定がない

position: sticky だけでは貼り付きません。 どこで止めるかを top・bottom・left・right のどれかで指定して初めて動作します(実測 1:-600 まで流れた)。

/* 直す前 */
.header { position: sticky; }

/* 直した後 */
.header { position: sticky; top: 0; }

原因2:祖先に overflow: hidden / auto / scroll がある

overflow が visible・clip 以外の祖先は「スクロールコンテナ」になり、sticky はその中で止まろうとします。 その祖先自体は高さが伸びてスクロールしないため、ページをスクロールすると一緒に流れます(実測 2a〜2c)。

見落としやすいのが overflow-x: hidden だけの指定です。 片方の軸だけ hidden にすると、もう片方の visible は auto として計算されるため、スクロールコンテナになります。横はみ出し対策でレイアウトの外枠に overflow-x: hidden を付けると、内側の sticky がすべて流れます。

body だけに付けた場合は例外です(実測 2f)。 body の overflow は html が visible のときビューポートに引き継がれるため、body はスクロールコンテナになりません。ただし html と body の両方に付けると、body がスクロールコンテナになり効かなくなります(実測 2g)。

修正は overflow: clip への置き換えが手軽です。 clip ははみ出しを切り取るだけで、スクロールコンテナを作りません(実測 2d・2e で固定された)。

/* 直す前:横はみ出し対策で外枠に hidden */
.layout { overflow-x: hidden; }

/* 直した後:clip にすると sticky が効く */
.layout { overflow-x: clip; }
値 はみ出しの切り取り スクロールコンテナになるか sticky への影響
visible しない ならない なし
clip する ならない なし
hidden する なる(プログラムからはスクロール可) その中で止まる
auto / scroll スクロールバー なる その中で止まる

overflow: clip は Chrome 90、Firefox 81、Safari 16 以降で使えます。clip ではプログラムからのスクロール(scrollTo)もできなくなるので、JS で横スクロールさせている要素には使えません。

原因3:親の高さが sticky 要素と同じ

sticky 要素は親要素の中でしか動けません。 親に sticky 要素しか入っていない(親の高さ=子の高さ)と、動ける余白が 0 なので流れます(実測 3)。ヘッダーを <div class="header-wrap"> で包み、その div ではなく中身に sticky を付けているケースでよく起きます。

<!-- 直す前:sticky の親が短い -->
<div class="header-wrap">
  <header class="header" style="position: sticky; top: 0">...</header>
</div>
<main>...</main>

<!-- 直した後:sticky は長い親(body)の直下の要素に付ける -->
<header class="header" style="position: sticky; top: 0">...</header>
<main>...</main>

ラッパーを外せない場合は、ラッパー自体に position: sticky; top: 0; を付けます。

原因4・5:flex / grid の stretch で高さが伸びている

flex と grid の子は、既定で行の高さいっぱいに伸びます(align-items: stretch)。 サイドバーが本文と同じ高さになるため、動ける余白がなくなります(実測 4a・5a)。sticky を付ける子だけ、高さを中身に合わせます。

/* flex の場合 */
.row { display: flex; }
.sidebar { position: sticky; top: 0; align-self: flex-start; }

/* grid の場合 */
.grid { display: grid; grid-template-columns: 240px 1fr; }
.sidebar { position: sticky; top: 0; align-self: start; }

原因6:表の外側に overflow-x: auto がある

表見出しの sticky は、th・thead・tr のどれに付けても Chromium 141 では効きます(実測 6a〜6c)。 効かない典型は、スマホで横スクロールさせるために表を overflow-x: auto の div で包んでいるケースです(実測 6d)。原因2と同じく、その div がスクロールコンテナになります。

対処 ページ全体のスクロールで固定 横スクロール 実測
外側 overflow-x: auto のまま されない(th の top -600) できる 固定されない
外側を overflow-x: clip に される(th の top 0) できない(はみ出しは切り取り) 固定された
外側に max-height: 400px; overflow: auto 外側の中をスクロールすると固定 できる 外側を 600px スクロールして th の top 0

横スクロールが必要な表は、外側に max-height を付けて「表の中でスクロールさせる」設計にすると、横スクロールと見出し固定を両立できます(後述のコピペ例)。

原因7:親要素自体が短い(仕様どおりの動き)

sticky は親の下端に達すると一緒に流れます。 高さ 300px のセクション内の 40px の見出しは、200px スクロール時点では 0 で止まり、600px では -340 になりました(実測 7)。章ごとの見出しを次の章で入れ替える用途では、これが望ましい動きです。ページの最後まで止めたい場合は、sticky を付ける要素をセクションの外(長い親の直下)に出します。

原因8:display: inline で親が短い

inline 要素も sticky になりますが、包含ブロックは親の p などになります。 p が1行分の高さしかないため、余白がなく流れます(実測 8a)。親が長ければ inline でも固定されました(実測 8b)。inline 自体が原因ではなく、原因3と同じ「親が短い」問題です。 ボタンやラベルを貼り付けたい場合は、display: block(または inline-block)にして、長い親の直下に置きます。

原因9:z-index で後ろの要素に隠れる

sticky は止まっているのに、後ろの position 付き要素(カードなど)が上に重なって見えないケースです。 300px スクロールした時点で、ヘッダーの top は 0(止まっている)ですが、画面上端の要素を document.elementFromPoint(100, 10) で調べると次の結果でした。

ヘッダーの CSS ヘッダーの top 画面上端にある要素
position: sticky; top: 0; 0 カード(position: relative の要素)
position: sticky; top: 0; z-index: 10; 0 ヘッダー

後から出てくる position 付き要素は、同じ z-index なら上に描かれます。 sticky ヘッダーには z-index を付けておくと安心です。

原因10:スクロールしているコンテナの取り違え

sticky は「最も近いスクロールコンテナ」の中でだけ止まります。 height: 300px; overflow: auto の div の中に sticky を置いた場合の実測です。

操作 sticky の top 結果
ページ(window)を 100px スクロール -99 div ごと流れる
div の中を 200px スクロール 1(div の上端 0+枠線 1px) div の上端で固定

iframe の中の sticky も同じ考え方で、iframe の中のスクロールでだけ止まります。 親ページを 100px スクロールすると、iframe 内の座標では top 0 のままですが、iframe 自体が -100 に移動するので画面上は流れて見えます。iframe 内を 500px スクロールすると、top 0 で固定されました。

参考:transform・filter・contain は sticky を妨げない

「祖先に transform があると sticky が効かない」という説明を見かけることがありますが、Chromium 141 では transform・will-change・filter・contain: paint・display: contents のいずれも、sticky の固定に影響しませんでした(実測の参考行)。これらは position: fixed の基準を変えるプロパティで、sticky の基準(スクロールコンテナ)は変えません。原因を探すときは overflow を優先して確認すると早く見つかります。


祖先の overflow を洗い出すコンソール用 JS スニペット

sticky が効かない要素を DevTools の Elements パネルで選択し、Console に貼り付けて実行します。 $0 は Elements で選択中の要素を表します。

(el => {
  const rows = [];
  for (let p = el.parentElement; p; p = p.parentElement) {
    const cs = getComputedStyle(p);
    if (cs.overflowX !== 'visible' || cs.overflowY !== 'visible') {
      rows.push({
        element: p.tagName.toLowerCase() + (p.id ? '#' + p.id : '') +
                 (p.className ? '.' + [...p.classList].join('.') : ''),
        overflowX: cs.overflowX,
        overflowY: cs.overflowY,
        scrollContainer: !['visible', 'clip'].includes(cs.overflowX) ||
                         !['visible', 'clip'].includes(cs.overflowY),
      });
    }
  }
  console.table(rows);
  return rows;
})($0);

実行結果(Chromium 141):次の HTML で #t を選んで実行しました。

<style>
  .layout { overflow-x: hidden; }
  main { overflow: clip; }
  #t { position: sticky; top: 0; }
</style>
<div class="layout" id="app">
  <main><div id="t">H</div>...</main>
</div>
element overflowX overflowY scrollContainer
main clip clip false
div#app.layout hidden auto true

scrollContainer: true の行が sticky の基準になっている要素です。 この例では overflow-x: hidden だけを書いた .layout の overflowY が auto と計算されていることもわかります。.layout を overflow-x: clip に変えて再実行すると、div#app.layout は overflowX: clip/overflowY: visible/scrollContainer: false となり、sticky が効くようになりました。


DevTools での確認手順

手順 操作(Chrome DevTools) 見るところ
1 sticky 要素を右クリック →「検証」 Elements で要素が選択される
2 Styles パネルで position: sticky が取り消し線になっていないか 上書き・タイプミスの有無
3 Computed タブで top を検索 auto なら原因1
4 上の JS スニペットを Console で実行 scrollContainer: true の祖先(原因2・6・10)
5 親要素を選び、Elements の青いハイライトで高さを見る 親と子の高さが同じなら原因3・4・5・8
6 Console で document.elementFromPoint(100, 10) sticky 以外が返れば原因9
7 Console の実行コンテキスト(上部のプルダウン)を確認 iframe 内なら原因10

コピペで使える sticky の例

sticky ヘッダー

<header class="site-header">サイト名</header>
<main>...長い本文...</main>
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;          /* 後ろのカード等に隠れないように */
  background: #fff;     /* 透明だと下の文字が透ける */
  border-bottom: 1px solid #ddd;
}
/* アンカーリンクで見出しがヘッダーの下に隠れないように */
html { scroll-padding-top: 64px; }

sticky サイドバー(grid)

<div class="layout">
  <aside class="sidebar">目次</aside>
  <article>...長い本文...</article>
</div>
.layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 24px;
  overflow-x: clip;     /* はみ出し対策は hidden ではなく clip */
}
.sidebar {
  position: sticky;
  top: 80px;            /* ヘッダーの高さ+余白 */
  align-self: start;    /* stretch を解除 */
  max-height: calc(100vh - 100px);
  overflow-y: auto;     /* サイドバーが画面より長いときは中でスクロール */
}

上のヘッダーとサイドバーの CSS を Chromium 141 で読み込み、1500px スクロールしたところ、ヘッダーは top 0、サイドバーは top 80 で固定され、サイドバーは高さ 500px(画面 600px-100px)で中がスクロール可能になりました。

表の見出し行(横スクロール対応)

<div class="table-wrap">
  <table>
    <thead><tr><th>品名</th><th>在庫</th><th>更新日</th></tr></thead>
    <tbody>...</tbody>
  </table>
</div>
.table-wrap {
  max-height: 70vh;     /* ここで縦スクロールさせる */
  overflow: auto;       /* 横スクロールも可能 */
}
.table-wrap th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #f5f5f5;
}
/* 1列目も固定したい場合 */
.table-wrap th:first-child,
.table-wrap td:first-child {
  position: sticky;
  left: 0;
  background: #fff;
}
.table-wrap thead th:first-child { z-index: 2; }

border-collapse: collapse の表では、sticky の th の枠線がスクロール時に残らないことがあります。 枠線が消える場合は border-collapse: separate; border-spacing: 0; にして、th に box-shadow: inset 0 -1px #ccc などで線を描くと安定します。


よくある質問

Q. position: sticky と position: fixed はどう使い分けますか? A. sticky は文書の流れに場所を残したまま、スクロールで止まります。 fixed は流れから外れて常に画面に固定されるため、下のコンテンツに padding で余白を作る必要があります。ヘッダーや表見出しは sticky の方が扱いやすいです。

Q. 横はみ出し対策の overflow-x: hidden を外せません。どうすればよいですか? A. overflow-x: clip に置き換えると、はみ出しは切り取ったまま sticky が効きます(実測 2e)。JS でその要素をスクロールさせていない場合に使えます。

Q. sticky を bottom に固定できますか? A. できます。bottom: 0 を指定すると、要素が画面下端より下にあるあいだ下端に貼り付きます。フッターの操作ボタンなどに使えます。

Q. スマホの Safari で動きが違います。 A. overflow: clip は Safari 16 以降で対応しています。それより前の Safari が対象の場合は、overflow を外すか、表のように「外側の要素の中でスクロールさせる」設計を選ぶと確実です。


まとめ

  • sticky は「最も近いスクロールコンテナ」の中で、「親要素の範囲内」でだけ止まります。
  • top などの指定がないと貼り付きません。 まず top: 0 を確認します。
  • 祖先の overflow: hidden / auto / scroll、overflow-x: hidden だけの指定も原因になります。 overflow: clip に置き換えると効きます(実測)。
  • flex / grid のサイドバーは align-self: flex-start(grid は start)で stretch を解除します。
  • 表は th・thead・tr のどれでも効き、外側の overflow-x: auto が原因になりやすいので、外側に max-height を付けて中でスクロールさせます。
  • 止まっているのに見えないときは z-index、ページを動かしても止まらないときはスクロールしている要素の取り違えを確認します。
  • transform・filter・contain は Chromium 141 では sticky に影響しませんでした。 原因探しは overflow を優先すると早く見つかります。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「position: stickyが効かない原因10選|チェックリストと修正コード付き」

株式会社RJCでは、CSS レイアウトの仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?