🧲

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 を外す/分割
対策Boverflow: 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 を親に入れる

cliphidden と同様にはみ出しを非表示にしますが、プログラム的スクロールもスクロールバーも作らないのが決定的な違い。
そのため多くのケースで 「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列にし、右列の .asideposition: 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”は完成

🔗 参考リンク

Discussion