position: sticky が効かない?その理由は overflow にあり。— overflow: clip で解決するCSS設計
✋ はじめに
「JSで実装していた追従ヘッダーやサイドナビ、今はCSSだけでできます」。
…のはずが、position: sticky
が効かない— そんな経験、ありませんか?
犯人はたいてい overflow
を持つ祖先要素。
本記事では sticky
の仕組みと、overflow: hidden
の“罠”、そして overflow: clip
での安全設計までを、実例コードとチェックリストでまとめます。
🧲 結論(先に)
-
sticky
は **最も近い「スクロール機構を持つ祖先」**に相対して張り付く - 祖先に
overflow: hidden/auto/scroll
があると、そこで張り付き先が切り替わる - その結果、ビューポートに張り付かない/見かけ上“効かない” ことが起きる
-
overflow: clip
はスクロールコンテナを作らないため、sticky
を壊しにくい - まずは
top
(または論理プロパティ)を必ず指定。それでもダメなら 祖先の overflow を疑う
🧐 仕組みを掴む
sticky
は通常は relative
と同様に流れに居続け、**しきい値(例:top: 0
)**に達したらそこに“張り付く”挙動です。
最低限は「position: sticky; top: 0;
」。まずはシンプルなヘッダーで確認しましょう。
<header class="site-header">Sticky Header</header>
<main class="content">
<p>…long content…</p>
</main>
.site-header {
position: sticky;
top: 0; /* しきい値(必須) */
z-index: 10; /* 重なり順で埋もれないように */
background: white;
border-bottom: 1px solid #ddd;
}
.content { min-block-size: 100svh; }
😤 典型的な“効かない”パターン
親や祖先に overflow: hidden;
を入れると、その要素がスクロールコンテナ扱いになり、sticky
はビューポートではなくその祖先に対して張り付きます。
ビューポートをスクロールしても張り付かない=「壊れたように見える」わけです。
対策A:祖先の
overflow: hidden
を外す/分割
対策B:overflow: clip
に置き換え(はみ出しは隠すがスクロールコンテナを作らない)
対策C:あえて“スクロールさせたい要素”側にoverflow: auto;
を付け、その中でsticky
させる
<section class="wrap-hidden">
<div class="sticky box">sticky(hidden祖先)</div>
<div class="spacer">…long content…</div>
</section>
<section class="wrap-clip">
<div class="sticky box">sticky(clip祖先)</div>
<div class="spacer">…long content…</div>
</section>
.wrap-hidden { overflow: hidden; border: 1px dashed; }
.wrap-clip { overflow: clip; border: 1px dashed; }
.sticky {
position: sticky;
top: 0.5rem;
background: #fff;
}
.spacer { block-size: 200vh; }
overflow: clip
を親に入れる
☺️ clip
は hidden
と同様にはみ出しを非表示にしますが、プログラム的スクロールもスクロールバーも作らないのが決定的な違い。
そのため多くのケースで 「stickyがビューポート基準で素直に効く」 ようになります。
(古いブラウザ向けには overflow: hidden
をフォールバックで併記も可)
<section class="wrap-hidden">
<div class="sticky box">sticky(hidden祖先)</div>
<div class="spacer">…long content…</div>
</section>
<section class="wrap-clip">
<div class="sticky box">sticky(clip祖先)</div>
<div class="spacer">…long content…</div>
</section>
.wrap-hidden { overflow: hidden; border: 1px dashed; }
.wrap-clip { overflow: clip; border: 1px dashed; }
.sticky {
position: sticky;
top: 0.5rem;
background: #fff;
}
.spacer { block-size: 200vh; }
/* 併記フォールバック:古め環境向けに hidden → clip */
.wrap-clip { overflow: hidden; overflow: clip; }
💡 レイアウト別の実用パターン
1) 追従ヘッダー
最小構成は position: sticky; top: 0;
。背後に重なり負けるなら z-index
を付与。
モバイルでは高さを min-block-size: 56px
などで固定し、100svh(アドレスバー変動対策)と併用も◎。
<header class="site-header">Sticky Header</header>
<main class="content">
<p>…long content…</p>
</main>
.site-header {
position: sticky;
top: 0; /* しきい値(必須) */
z-index: 10; /* 重なり順で埋もれないように */
background: white;
border-bottom: 1px solid #ddd;
}
.content { min-block-size: 100svh; }
2) 2カラムでサイドナビだけ固定
親をGridで2列にし、右列の .aside
に position: sticky; top: 1rem;
。
この時、列親や祖先に overflow: hidden
を入れない(入れるなら clip
へ)。
<div class="two-cols">
<main class="main">
<p>…content…</p>
</main>
<aside class="aside">
<nav class="toc">…目次…</nav>
</aside>
</div>
.two-cols {
display: grid;
grid-template-columns: 1fr minmax(16rem, 24rem);
gap: 2rem;
}
.aside { position: sticky; top: 1rem; align-self: start; }
3) 独自スクロール領域内での sticky
「メインだけ縦スクロール」にしたい場合、そのメインに overflow: auto
を与えます。
sticky
は その要素内 で効くので、対象の .sticky
が同じスクロール領域内にいることを確認。
<section class="panel">
<header class="panel__head">Title</header>
<div class="panel__body">
<div class="panel__sidebar sticky-inpanel">Filters</div>
<div class="panel__content">…long content…</div>
</div>
</section>
.panel__body {
display: grid;
grid-template-columns: 16rem 1fr;
overflow: auto; /* ← この領域がスクロール */
max-block-size: 80svh;
}
.sticky-inpanel { position: sticky; top: 0.5rem; }
☑️“効かない”ときのデバッグ用チェックリスト
-
sticky要素に
top
(またはinset-block-start
)が指定されている -
祖先に
overflow: hidden/auto/scroll
が付いていない(付いていたら**clip
検討**) - sticky要素がスクロール領域の直下にある(別領域にまたがっていない)
-
重なり順で負けていない(
z-index
とスタッキングコンテキストを確認) - テーブル・変則レイアウトの特殊挙動を踏んでいない(仕様上の制約を確認)
👍 張り付いた状態”の見た目を変える
スクロール状態コンテナクエリの stuck
を使うと、張り付き中だけ影や背景色を変える等の表現が可能。
JSなしで「張り付いたら段差に影」などが実現できます。
@container style(--stuck: true) {
.site-header { box-shadow: 0 4px 8px rgba(0,0,0,.08); }
}
/* sticky要素をコンテナにする(例) */
.site-header { container-type: style; }
🎯 まとめ
-
sticky
はスクロールコンテナの境界で挙動が決まる - 祖先に
overflow: hidden/auto/scroll
があると効かなく見えるのは仕様どおり - まずは**
clip
で“隠すがスクロールは作らない”**に置き換えを検討 - それでも必要なら、その領域内スクロール+その中でのstickyへ設計変更
- 最後に
stuck
クエリで演出強化までやれば“脱JSの追従UI”は完成
🔗 参考リンク
- MDN: position(stickyの張り付き先=“スクロール機構を持つ祖先”の説明)
-
MDN: overflow(
hidden
はスクロール可、clip
は不可=スクロールコンテナを作らない) - MDN: Container scroll-state queries(
stuck
) - 解説: Polypane 「Getting stuck: all the ways
position: sticky
can fail」 - 解説: CSS-Tricks 「Dealing with overflow and
position: sticky
」
Discussion