サイドバー、目次、ヘッダー、追従バナーなどをスクロールに合わせて画面内に留めるためにposition: sticky;を指定したものの、「なぜか固定されずにそのまま流れてしまう」「途中で外れて上へ消えてしまう」といった現象に遭遇したことはないでしょうか。

position: sticky;は非常に便利なCSSプロパティですが、要素単体の指定だけでなく、祖先要素のoverflow、親要素の高さ、Flexbox/Gridの配置プロパティなど、周囲のレイアウト構造に強く影響を受ける特徴を持っています。

この記事では、position: sticky;が効かない・動いてしまう主な原因と、DevToolsを使った確実な切り分け手順を解説します。

position: stickyが効かないときにまず確認すること

コードを細かく見直す前に、まずは以下の基本ポイントを上から順に確認してみてください。

  1. topやbottomなどのしきい値(位置指定)を書いているか
  2. 親要素やさらに上の祖先要素にoverflow: hiddenなどが指定されていないか
  3. 親要素にスクロール移動できるだけの十分な縦幅(高さ)があるか
  4. FlexboxやGridのstretch配置で要素が引き伸ばされていないか
  5. 固定されているが他の要素の下に潜り込んでいないか(重なり順/z-index)

position: stickyの基本的な動きを理解する

トラブルの原因を切り分けるために、まずposition: sticky;がどのように動作するのかを確認します。

.sidebar {
  position: sticky;
  top: 20px;
}

stickyは最初から固定されるわけではない

position: sticky;を指定した要素は、初期状態では通常フローに従って配置されます。

ページをスクロールし、要素が表示領域の上端からtop: 20pxの位置(しきい値)に達した瞬間に、その位置を保つように固定(追従)状態へと切り替わります。

fixedとは動き方が違う

  • position: fixed;:通常フローから外れ、通常はブラウザの表示領域(ビューポート)を基準に固定されます。
  • position: sticky;:通常フロー内に留まりながら、スクロール祖先を基準に追従し、包含ブロック(多くの場合はレイアウト上の親要素)の範囲内に収まります。
【position: sticky の追従範囲】
┌──────────────────────────────────────────────┐
│ [親要素 (コンテナ)]                          │
│                                              │
│  ┌────────────────────┐                      │
│  │ sticky要素         │ ← top: 20px に達すると追従開始
│  └────────────────────┘                      │
│                                              │
│                                              │
│  ..........................................  │ ← 親要素の下端
└──────────────────────────────────────────────┘
   ↑ 親要素の範囲を抜けると、一緒に上へスクロールして消える

sticky要素は包含ブロック(親要素)の範囲を飛び出して画面に残り続けることはできません。

この「親要素の枠内だけで動く」という原則が、トラブルシューティングの鍵になります。

原因1:topなどの位置(しきい値)を指定していない

position: sticky;を指定しても全く固定されない場合、最も多いのがtop、bottom、left、rightのいずれも指定していないケースです。

/* ✕ 効かない例(しきい値がない) */
.sidebar {
  position: sticky;
}

/* ◯ 改善例(上端から20pxで固定) */
.sidebar {
  position: sticky;
  top: 20px;
}

MDNの仕様解説にもある通り、sticky動作を成立させるには、追従させたい軸のインセットプロパティ(垂直方向ならtopまたはbottom)にauto以外の値を指定する必要があります。

位置が未指定(auto)のままだと、ブラウザはどの位置で追従を開始すべきか判断できず、実質的にposition: relative;と同じようにそのまま流れてしまいます。

※固定ヘッダーがあるサイトでその真下に追従させたい場合は、top: 80px;のようにヘッダーの高さ分を考慮した値を設定します。

原因2:親・祖先要素のoverflowが影響している

CSSの記述上はposition: sticky;もtop: 20px;も正しく書けているのに動かない場合、本命となる原因が祖先要素のoverflowプロパティです。

/* 祖先のラッパー要素 */
.wrapper {
  overflow: hidden; /* 意図しないスクロール祖先になり、stickyの動きに影響することがある */
}

親要素や祖先要素にoverflow: hidden;、overflow: auto;、overflow: scroll;などが指定されていると、その祖先要素がstickyの基準となるスクロール祖先になることがあります。

そのため、ページ全体のスクロールを基準に追従させたつもりでも、期待どおりに動かない場合があります。

なお、overflow: clip;はhiddenと異なりスクロールコンテナを作りません。

単にあふれた内容を切り取りたいだけの場合は、用途に応じてclipが選択肢になることもあります。

祖先要素のoverflowを特定する手順

  1. ブラウザの「開発者ツール(DevTools)」を開く
  2. position: sticky;を付けた要素を選択する
  3. DOMツリーを親、親の親……と<body>に向かって1階層ずつ辿る
  4. 各要素のスタイルを確認し、意図しないoverflow(特にhiddenやauto)が付いていないかチェックする

※ DevToolsではoverflowだけでなく、overflow-xとoverflow-yの算出値も確認してください。

片方の軸の指定によって、もう片方がautoとして扱われる場合があります。

不要なoverflow: hidden;であれば削除し、横スクロール防止などで入れている場合は影響しない別の階層へ移動させるなどの見直しを行います。

原因3:親要素の高さが足りず固定できる距離がない

「stickyがほんの一瞬しか効かない」「指定したのにすぐ外れてしまう」という場合、親要素の高さがsticky要素とほぼ同じになっている可能性があります。

<!-- ✕ 効かない構造の例 -->
<div class="main-content">本文(長い)</div>

<div class="sidebar-wrapper"> <!-- この親要素がサイドバーと同じ高さしかない -->
  <aside class="sidebar">追従させたい目次</aside>
</div>

前述の通り、sticky要素は「包含ブロック(親要素)の終端(下端)」まで到達すると固定が解除され、親要素と一緒に上へ流れていきます。

親要素の高さがsidebarと同じであれば、そもそも下に移動できる余白(スクロールできる距離)がゼロなため、全く固定されていないように見えます。

<!-- ◯ 追従できる構造の例 -->
<div class="container"> <!-- 本文とサイドバーを包む共通の親要素 -->
  <main class="main-content">本文(長い)</main>
  <aside class="sidebar">追従させたい目次</aside>
</div>

原因4:flexやgridで要素の高さが引き伸ばされている

2カラムレイアウトでFlexboxやCSS Gridを使っている際にも、stickyが効かなくなる典型パターンがあります。

.container {
  display: flex; /* または display: grid; */
}

.sidebar {
  position: sticky;
  top: 20px;
}

FlexboxやCSS Gridでは、stretchの配置によってsticky要素が利用可能な高さまで引き伸ばされることがあります。

2カラムレイアウトでは、その結果としてサイドバーが本文側と同程度の高さになり、親要素内で移動できる余地がなくなる場合があります。

対策:align-selfで高さを中身に合わせる

サイドバー側にalign-self: flex-start;を指定して、要素自身の高さを中身のコンテンツサイズに合わせます(Gridの場合はalign-self: start;などでstretchを解除します)。

.sidebar {
  position: sticky;
  top: 20px;
  align-self: flex-start; /* 高さが親全体に引き伸ばされるのを防ぐ */
}

これによってサイドバーの下に親要素の余白が生まれ、本文の長さに合わせてスムーズに追従するようになります。

原因5:sticky要素自身が高すぎる

サイドバーのリンク数が多い、バナーが縦に長いなど、sticky要素自体の高さが表示領域(ビューポート)より大きい場合も注意が必要です。

例えば画面の高さが800pxなのに、サイドバーが1000pxある状態では、stickyで追従している間にサイドバーの一部が表示領域の外へはみ出し、下側の項目を確認しづらくなることがあります。

sticky要素が表示領域より大きい場合は、単純に上端へ固定するだけでは使いにくくなるため、要素の高さや内部スクロールもあわせて設計する必要があります。

対策:

  • stickyにする対象をサイドバー全体ではなく「目次ブロックだけ」などに絞る
  • サイドバー内部にmax-height: calc(100vh - 40px);とoverflow-y: auto;を指定し、サイドバー単体でスクロールできるようにする

position: stickyなのにスクロールすると動くのはなぜ?

検索時によくある疑問として「stickyを指定したのに、スクロールしたら動いてしまう」というものがあります。

しかし、「スクロール中に動く」こと自体は、stickyの仕様として正常な動作であるケースが多いです。

1.topのしきい値に達するまでは動く(正常)

初期位置からスクロールして指定したtop(例: 20px)に達するまでは、通常の要素と同じように上へスクロール移動します。

2.包含ブロックの終端に達したら再び動く(正常)

包含ブロック(多くの場合は親要素)の下端に追いついた後は、親要素の外へ飛び出さないよう固定が解除され、親要素と一緒に上へ消えていきます。

「最後まで画面にずっと居座る」わけではありません。

3.画面にずっと残したいならfixedを検討する

もし「記事の終わりに関係なく、画面の右下やヘッダーに常に常駐させたい」という要件であれば、stickyではなくposition: fixed;を使うのが適切な設計です。

固定されているのに見えない場合はz-indexを確認する

「スクロールすると固定位置には留まっているようだが、メインコンテンツの画像や見出しの下に潜り込んで見えなくなる」という場合は、重なり順(z-index)が原因です。

.sticky-header {
  position: sticky;
  top: 0;
  z-index: 10; /* 重なり順を前面に設定 */
}

position: sticky;を適用した要素は、新しいスタッキングコンテキスト(重ね合わせコンテキスト)を生成します。

後続の要素(スライダーやposition: relativeが付いた要素など)より前面に表示させたい場合は、適切なz-indexを指定してください。

※z-indexを上げても前面に出ない場合は、祖先要素側に別のスタッキングコンテキストが作られていないかも確認します。

position: stickyが効かないときの確認手順

トラブルに遭遇した際は、以下のステップで1つずつ切り分けを行うと原因を素早く特定できます。

  1. 基本指定の確認:position: sticky;とともにtop(またはbottom)が指定されているか
  2. 祖先のoverflow確認:DevToolsで親〜<body>までの階層にoverflow: hidden / auto / scroll(およびoverflow-x / yの算出値)がないか確認
  3. 親要素の高さ確認:親要素がsticky要素よりも十分に縦に長いか(移動できる余白があるか)
  4. Flex / Gridの確認:display: flex / gridの親の中にいる場合、align-self: flex-start;やalign-self: start;を入れているか
  5. 要素自身のサイズ確認:sticky要素自身が表示領域より縦に長すぎないか
  6. 重なり順の確認:他の要素の下に潜っていないか(z-indexおよび祖先のスタッキングコンテキストの確認)

sticky・fixed・relativeの違い

用途に応じたpositionの使い分けの整理です。

プロパティ 通常フロー 固定・配置の基準 主な用途・向いているUI
relative 残る 本来配置されるはずだった自身の位置 少しの位置微調整、子要素のabsoluteの基準
sticky 残る スクロールする祖先要素&包含ブロック(親要素)の範囲内 記事の目次、サイドバー追従、テーブルの見出し固定
fixed 外れる 通常はブラウザの表示領域(ビューポート) 常時固定するヘッダー、トップへ戻るボタン、モーダル

まとめ

position: sticky;が効かない・意図通りに動かないときは、対象要素のCSSだけでなく「親要素(包含ブロック)のサイズ」と「祖先要素のスクロール設定」に目を向けることが解決への最短ルートです。

  • しきい値の必須:topなどの位置指定を省略すると追従しない
  • 祖先のoverflow:親や祖先にoverflow: hiddenなどがあると意図しないスクロール祖先になる場合がある(clipはスクロールコンテナを作らない)
  • 親要素の範囲:stickyは包含ブロックの範囲内でのみ追従し、親の終端で止まる(動くのは正常仕様)
  • Flexbox対策:高さが引き伸ばされて動かない場合はalign-self: flex-start;を指定する

「動かないからとりあえずposition: fixedやJavaScriptに変える」とする前に、まずは上記のチェックリストに沿ってレイアウト構造を確認してみてください。