position: sticky; top: 0; と書いた。書いたのに、スクロールすると要素はそのまま流れていく。DevToolsで見てもタイポはない。ブラウザの対応表を見ても全部緑。
これ、ほぼ100%あなたのCSSではなく「親要素」が原因です。stickyは自分だけでは完結せず、祖先の状態に強く依存する珍しいプロパティだからです。
この記事では発生频度の高い順に5つの原因を並べ、それぞれ「壊れている例」と「直した例」を並べたCodePenを埋め込んでいます。上から順に確認していけば、だいたい3番目までで解決するはずです。
大前提:stickyは「最も近いスクロールコンテナ」の中でしか動けない
原因を見る前に、この1行だけ頭に入れてください。
sticky要素は、自分の最も近い「スクロールコンテナ」の内側に閉じ込められる。
スクロールコンテナとは、overflow が visible 以外になっている要素のことです。hidden も auto も scroll も、全部スクロールコンテナになります。
そして重要なのが次の点です。
- sticky要素は、そのスクロールコンテナがスクロールしたときにだけ張り付く
- スクロールコンテナ自体にスクロールする中身がなければ、stickyは一生発動しない
「効かない」と感じるケースの大半は、この「発動しないスクロールコンテナ」に閉じ込められている状態です。
原因1:祖先のどこかに overflow: hidden がある(最多)
いちばん多いのがこれです。しかも厄介なのは、直近の親でなくても壊れること。5階層上のラッパーに横スクロール防止で書いた overflow-x: hidden が犯人、というのが定番パターンです。
/* 遠い祖先にこれがあるだけで壊れる */
.container {
overflow: hidden; /* ← 犯人 */
}
.bar {
position: sticky;
top: 0; /* 効かない */
}直し方:overflow: hidden を overflow: clip にする
overflow: clip は「はみ出しを切り取る」という点では hidden と同じですが、スクロールコンテナを作りません。だから中のstickyが生き残ります。
.container {
/* overflow: hidden; */
overflow: clip; /* ← これでstickyが生きる */
}横方向だけ切りたい場合は overflow-x: clip でOKです。overflow-x: hidden と違い、overflow-y が勝手に auto に化けることもありません(これも地味にハマるポイントです)。
overflow: clip は2021年から全モダンブラウザで使えるので、実務でそのまま使って問題ありません。
犯人の overflow を一発で探すスニペット
手で祖先を辿るのは骨が折れるので、DevToolsのコンソールにこれを貼ってください。sticky要素を渡すと、祖先にあるスクロールコンテナを全部リストアップします。
function findScrollParents(el) {
const hits = [];
let node = el.parentElement;
while (node) {
const s = getComputedStyle(node);
const values = `${s.overflowX} ${s.overflowY}`;
if (/(auto|scroll|hidden|overlay)/.test(values)) {
hits.push({ el: node, overflow: values });
}
node = node.parentElement;
}
return hits;
}
// 使い方
findScrollParents(document.querySelector('.bar')).forEach(h => {
console.log(h.overflow, h.el);
});hidden と出た要素が犯人です。clip に変えて解決するか試してください。
原因2:top / bottom / left / right を1つも指定していない
position: sticky は単体では何もしません。「どこに張り付くか」を示すオフセットが必須です。
/* ❌ 何も起きない */
.bar { position: sticky; }
/* ✅ 上端に張り付く */
.bar { position: sticky; top: 0; }意外と多いのが、top を書いたつもりでメディアクエリの外側だけに書いていたり、リセットCSSやフレームワークのユーティリティクラスに top: auto で打ち消されているケースです。DevToolsの Computed タブで top が auto になっていないか確認してください。
なお top と bottom を両方指定した場合は top が優先されます。
原因3:親の高さが sticky 要素とほぼ同じで、動く余地がない
stickyは「親要素の内側」でしか移動できません。親の高さがsticky要素の高さと同じなら、動ける距離がゼロなので、見た目には「効いていない」状態になります。
これが起きやすいのが Flexbox / Grid のサイドバーです。
/* ❌ サイドバー列が中身の高さまでしか伸びず、stickyが動けない */
.layout {
display: flex;
align-items: flex-start; /* ← 犯人 */
}
/* ✅ 列はデフォルトの stretch で全高に伸ばし、
中の要素をstickyにする */
.layout {
display: flex;
/* align-items: stretch; がデフォルト */
}
.sidebar__inner {
position: sticky;
top: 16px;
}ポイントは、「伸びる列」と「張り付く中身」を分けることです。列そのものに position: sticky を付けると、列が全高に伸びている限り動く余地がないので効きません。列の中に1枚ラッパーを噛ませて、そちらをstickyにします。
原因4:親に height: 100% や固定の overflow: auto が付いている
管理画面やアプリ系のレイアウトでよくあるのが、こういう構造です。
html, body { height: 100%; }
.app { height: 100%; overflow: auto; } /* ← ここがスクロールコンテナ */この場合、ページ全体をスクロールしているのは window ではなく .app です。stickyは .app を基準にするので、.app の中で正しくスクロールしていれば動きます。逆に、.app のさらに内側でもう1つ overflow: auto を作っていると、stickyの基準がそちらに移って想定と変わります。
ヘッダーが画面上端ではなく、なぜか途中で止まるときは、この「基準がひとつ内側のコンテナになっている」パターンを疑ってください。
また、overflow: auto を持つコンテナの中でstickyを使うときは、そのコンテナに height または max-height が必要です。高さの制限がないとコンテナは中身の分だけ伸び、スクロールが発生せず、やはりstickyは発動しません。
原因5:display の値が sticky と相性が悪い
display: inline の要素にstickyは効きません。また、以下は挙動が独特なので注意が必要です。
<thead>/<tr>… 現在は主要ブラウザで動きますが、border-collapse: collapseだとボーダーが一緒にスクロールして消える現象があります。border-collapse: separateにして、セルにbox-shadowで線を引くのが定番の回避策ですdisplay: contents… 要素自体がボックスを生成しないため、stickyは無効になります
実践編:ヘッダーと1列目を両方stickyにするテーブル
原因を潰したところで、いちばん需要が高い応用例をひとつ。「縦にスクロールしてもヘッダーが残り、横にスクロールしても1列目が残る」テーブルです。
コツは、ヘッダーと1列目を同じスクロールコンテナの中に入れること。ラッパーに max-height と overflow: auto を与え、thead th に top: 0、tbody th に left: 0 を指定します。交差する左上のセルだけは z-index を1段上げるのを忘れずに。
補足:もうすぐ「軸ごとに基準を分ける」書き方が来ます
上のテーブルはラッパー内でスクロールする形なので、「ページ全体をスクロールしながらヘッダーだけビューポート上端に固定したい」という要望には応えられません。従来はJavaScriptでスクロール同期するか、ヘッダーを複製するしかありませんでした。
これに対して、CSSの仕様側で 「横軸はラッパー、縦軸はドキュメント」というように軸ごとに別々のスクロールコンテナを基準にする 変更が入りました。書き方はこうです。
.table-wrap {
/* ❌ overflow-y が auto に化けて、縦のstickyが壊れる */
/* overflow-x: auto; */
/* ✅ 横だけスクロールコンテナ、縦はクリップのみ */
overflow: auto clip;
}overflow-x: auto を指定すると overflow-y も自動的に auto に計算される、というのが従来ハマりどころでした。clip を明示することで縦軸のスクロールコンテナ化を防げます。
ただし2026年7月時点では Chrome 148の実験的機能フラグ下でのみ動作 し、FirefoxとSafariは未対応です。Chrome 150からは以下の機能検出が使えるようになるため、本番投入はもう少し先を見ておくのが安全です。
@supports named-feature(single-axis-scroll-container) {
/* 単軸スクロールコンテナが使える環境 */
}チェックリスト
うまくいかないときは、上から順に。
- 祖先に
overflow: hidden | auto | scrollはないか → あればclipに置き換える top/bottom/left/rightのどれかを指定しているか → Computed でautoになっていないか確認- 親要素にsticky要素が動けるだけの高さがあるか → Flex/Gridなら
align-itemsがstretchか確認 overflow: autoのコンテナにheight/max-heightがあるか → なければスクロールが起きずstickyは発動しないdisplayがinlineやcontentsになっていないか- テーブルなら
border-collapse: separateになっているか
この6項目で、実務で遭遇するstickyの不具合はほぼ拾えます。
参考リンク
- Getting stuck: all the ways position:sticky can fail — Polypane
- CSS 'position: sticky' not working? Try 'overflow: clip', not 'overflow: hidden' — Terluin Webdesign
- CSS position: sticky now sticks to the nearest scroller on a per axis basis! — Bram.us
- A Table with Both a Sticky Header and a Sticky First Column — CSS-Tricks
- Prefill Embeds — CodePen Documentation