- CSS
position: stickyが効かない原因10選|チェックリストと修正コード付き
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期連続の黒字決算。