position: sticky; top: 0; と書いた。書いたのに、スクロールすると要素はそのまま流れていく。DevToolsで見てもタイポはない。ブラウザの対応表を見ても全部緑。

これ、ほぼ100%あなたのCSSではなく「親要素」が原因です。stickyは自分だけでは完結せず、祖先の状態に強く依存する珍しいプロパティだからです。

この記事では発生频度の高い順に5つの原因を並べ、それぞれ「壊れている例」と「直した例」を並べたCodePenを埋め込んでいます。上から順に確認していけば、だいたい3番目までで解決するはずです。

大前提:stickyは「最も近いスクロールコンテナ」の中でしか動けない

原因を見る前に、この1行だけ頭に入れてください。

sticky要素は、自分の最も近い「スクロールコンテナ」の内側に閉じ込められる。

スクロールコンテナとは、overflowvisible 以外になっている要素のことです。hiddenautoscroll も、全部スクロールコンテナになります。

そして重要なのが次の点です。

  • sticky要素は、そのスクロールコンテナがスクロールしたときにだけ張り付く
  • スクロールコンテナ自体にスクロールする中身がなければ、stickyは一生発動しない

「効かない」と感じるケースの大半は、この「発動しないスクロールコンテナ」に閉じ込められている状態です。

原因1:祖先のどこかに overflow: hidden がある(最多)

いちばん多いのがこれです。しかも厄介なのは、直近の親でなくても壊れること。5階層上のラッパーに横スクロール防止で書いた overflow-x: hidden が犯人、というのが定番パターンです。

/* 遠い祖先にこれがあるだけで壊れる */
.container {
  overflow: hidden; /* ← 犯人 */
}

.bar {
  position: sticky;
  top: 0; /* 効かない */
}

直し方:overflow: hiddenoverflow: 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 タブで topauto になっていないか確認してください。

なお topbottom を両方指定した場合は 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-heightoverflow: auto を与え、thead thtop: 0tbody thleft: 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) {
  /* 単軸スクロールコンテナが使える環境 */
}

チェックリスト

うまくいかないときは、上から順に。

  1. 祖先に overflow: hidden | auto | scroll はないか → あれば clip に置き換える
  2. top / bottom / left / right のどれかを指定しているか → Computed で auto になっていないか確認
  3. 親要素にsticky要素が動けるだけの高さがあるか → Flex/Gridなら align-itemsstretch か確認
  4. overflow: auto のコンテナに height / max-height があるか → なければスクロールが起きずstickyは発動しない
  5. displayinlinecontents になっていないか
  6. テーブルなら border-collapse: separate になっているか

この6項目で、実務で遭遇するstickyの不具合はほぼ拾えます。

参考リンク