position: stickyとは?基本の使い方
CSSのposition: stickyは、要素がスクロール位置に応じて相対配置(relative)から固定配置(fixed)に切り替わる、非常に便利なポジショニング方法です。
たとえば、ページをスクロールしてヘッダーが画面上部に到達したら、そのまま画面上部に固定される——という動きを、JavaScriptなしで実現できます。
position: sticky の基本動作
- 通常時は
position: relative と同じようにドキュメントフローに従う
- スクロールして指定した閾値(top, bottom など)に達すると
position: fixed のように固定される
- 親要素の範囲を超えると固定が解除される
- JavaScript不要でスクロール追従UIを実装できる
基本構文
position: stickyを使うには、最低限2つのプロパティが必要です。
CSS – 基本構文
.sticky-element {
position: sticky;
top: 0; /* スクロール時に画面上部に固定 */
}
position: stickyと、top(またはbottom、left、right)の指定が必須です。top: 0を指定すると、要素が画面の上端に到達した時点で固定されます。
実際の使用例
追従するヘッダーの最もシンプルな例を見てみましょう。
HTML
<div class="container">
<header class="sticky-header">
サイトヘッダー
</header>
<main>
<p>本文コンテンツ...(長いテキスト)</p>
</main>
</div>
CSS
.sticky-header {
position: sticky;
top: 0;
background: #ffffff;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.container {
min-height: 200vh; /* スクロールできる十分な高さ */
}
この例では、ページをスクロールすると.sticky-headerが画面上部に固定されます。親要素の.containerが十分な高さを持っているため、stickyが正常に動作します。
stickyの動作フロー
| スクロール状態 |
sticky要素の動作 |
相当するposition |
| 要素がまだ閾値に達していない |
通常のフロー内に配置 |
relative |
| 要素が閾値(top: 0)に達した |
画面に固定される |
fixed |
| 親要素の終端に達した |
固定が解除され、親と一緒にスクロール |
relative(末端) |
position: sticky と position: fixed の違い
stickyとfixedは一見似ていますが、重要な違いがあります。
| 比較項目 |
position: sticky |
position: fixed |
| ドキュメントフロー |
フロー内に残る(スペースを占有) |
フローから外れる(スペースを占有しない) |
| 基準となる要素 |
親要素(スクロールコンテナ) |
ビューポート |
| 固定の範囲 |
親要素の範囲内のみ |
常にビューポートに固定 |
| レイアウトへの影響 |
なし(元の位置にスペース確保) |
あり(要素が消えた分、後続要素がずれる) |
| JavaScript不要 |
はい |
はい |
| 主な用途 |
セクション内ヘッダー、サイドバー |
常時固定ヘッダー、フローティングボタン |
ポイント:position: stickyは「親要素の中でだけ固定される」というのが最大の特徴です。この仕組みを理解していないと、stickyが効かない原因を特定するのが難しくなります。
ブラウザ対応状況
position: stickyは現在、主要なモダンブラウザすべてでサポートされています。
| ブラウザ |
対応バージョン |
備考 |
| Chrome |
56+ |
完全対応 |
| Firefox |
32+ |
完全対応 |
| Safari |
13+ |
6.1+で -webkit-sticky として先行対応 |
| Edge |
16+ |
完全対応 |
| iOS Safari |
13+ |
一部制約あり(後述) |
| IE 11 |
非対応 |
polyfillが必要 |
注意:IE 11ではposition: stickyは完全に非対応です。IE対応が必要な場合は、JavaScriptによるpolyfillか、position: fixedでの代替実装が必要です。ただし、2025年以降はIE対応を求められるケースはほぼありません。
原因①:top / bottom / left / right の指定忘れ
position: stickyが効かない最も多い原因が、オフセット値(top, bottom, left, right)の指定忘れです。
なぜオフセット値が必要なのか
position: stickyは、「要素がビューポートの指定された位置に到達したら固定する」という仕組みです。そのため、どの位置で固定するかを明示する必要があります。
オフセット値がない場合、ブラウザは「どの位置で固定すればよいか」を判断できず、stickyとして機能しません。
問題のあるコード
CSS – NG: オフセット値なし
.header {
position: sticky;
/* top の指定がない → stickyが効かない */
background: #fff;
padding: 16px;
}
修正後のコード
CSS – OK: top: 0 を追加
.header {
position: sticky;
top: 0; /* これが必須 */
background: #fff;
padding: 16px;
}
各方向のオフセット指定
スクロール方向に応じて、適切なオフセット値を指定します。
| プロパティ |
固定される位置 |
使用例 |
top: 0 |
画面上端に固定 |
追従ヘッダー、ナビゲーション |
top: 20px |
画面上端から20pxの位置に固定 |
既存ヘッダーの下に固定 |
bottom: 0 |
画面下端に固定 |
追従フッター、CTAボタン |
left: 0 |
画面左端に固定 |
横スクロールテーブルの固定列 |
right: 0 |
画面右端に固定 |
横スクロール時の固定サイドバー |
複数方向の指定
複数のオフセット値を同時に指定することも可能です。
CSS – 複数方向の指定
/* 上下方向で固定範囲を制限 */
.sticky-element {
position: sticky;
top: 0;
bottom: 20px;
}
/* 横スクロールテーブルの最初の列を固定 */
.table-fixed-col {
position: sticky;
left: 0;
z-index: 1;
}
ポイント:縦スクロールのstickyにはtopまたはbottomを、横スクロールのstickyにはleftまたはrightを指定しましょう。最も一般的な「画面上部への固定」であればtop: 0が定番です。
原因②:親要素の overflow プロパティ
position: stickyが効かない原因として最も気づきにくいのが、親要素(または祖先要素)に設定されたoverflowプロパティです。
overflowがstickyを無効化する仕組み
CSS仕様では、position: stickyは最も近いスクロールコンテナ(scroll container)の中で固定されると定義されています。
overflow: hidden、overflow: auto、overflow: scrollのいずれかが設定された要素は、スクロールコンテナになります。sticky要素は、このスクロールコンテナの中でしか固定されません。
つまり、親要素にoverflow: hiddenが設定されている場合、その親要素自体がスクロールしないため、sticky要素は固定されるタイミングがなく、結果としてstickyが効いていないように見えるのです。
問題のあるコード
HTML
<div class="parent">
<div class="sticky-nav">ナビゲーション</div>
<div class="content">コンテンツ...</div>
</div>
CSS – NG: 親要素に overflow: hidden
.parent {
overflow: hidden; /* これが原因でstickyが効かない */
}
.sticky-nav {
position: sticky;
top: 0;
}
修正方法1:overflow を削除する
最もシンプルな解決策は、親要素のoverflowを削除することです。
CSS – OK: overflow を削除
.parent {
/* overflow: hidden を削除 */
}
.sticky-nav {
position: sticky;
top: 0;
}
修正方法2:overflow: clip を使う
overflow: hiddenの代わりにoverflow: clipを使えば、はみ出しの切り取りは維持しつつ、stickyを有効に保てます。
CSS – OK: overflow: clip を使用
.parent {
overflow: clip; /* hidden の代わりに clip を使う */
}
.sticky-nav {
position: sticky;
top: 0;
}
overflow: clip と overflow: hidden の違い
overflow: hidden — スクロールコンテナを生成するため、stickyが効かなくなる
overflow: clip — スクロールコンテナを生成しないため、stickyが正常に動作する
- どちらもはみ出したコンテンツを切り取る点は同じ
overflow: clipはChrome 90+, Firefox 81+, Safari 16+ で対応
修正方法3:overflow-x / overflow-y を個別指定する
横方向のみoverflowを制御したい場合は、overflow-xのみ指定します。
CSS – overflow-x のみ指定
.parent {
overflow-x: hidden; /* 横方向のみ非表示 */
/* overflow-y は指定しない(visible のまま) */
}
注意:overflow-x: hiddenを指定すると、ブラウザはoverflow-yを自動的にautoに変更する場合があります。この場合もスクロールコンテナが生成され、stickyが効かなくなる可能性があります。この問題を回避するにはoverflow-x: clipを使用してください。
祖先要素まで遡って確認する方法
stickyが効かない場合、直接の親だけでなく、すべての祖先要素を確認する必要があります。どこか1つでもoverflowが設定されていると、stickyが効かなくなります。
HTML – 祖先要素が原因のケース
<!-- この wrapper の overflow が原因 -->
<div class="wrapper" style="overflow: hidden;">
<div class="layout">
<div class="sidebar">
<!-- stickyが効かない -->
<div class="sticky-widget">
ウィジェット
</div>
</div>
</div>
</div>
Chrome DevToolsでの確認方法は以下の通りです。
DevTools Console – overflow を持つ祖先を検索
// sticky要素の祖先でoverflowが設定されている要素を探す
let el = document.querySelector('.sticky-widget');
while (el = el.parentElement) {
const style = getComputedStyle(el);
if (style.overflow !== 'visible') {
console.log('overflow発見:', el, style.overflow);
}
}
このスクリプトをDevToolsのConsoleに貼り付けて実行すると、overflowがvisible以外に設定されている祖先要素が一覧で表示されます。
overflow の各値とstickyへの影響
| overflow の値 |
stickyへの影響 |
スクロールコンテナ |
visible(デフォルト) |
影響なし(stickyが正常に動作) |
生成しない |
hidden |
stickyが効かなくなる |
生成する |
scroll |
親のスクロール内でのみstickyが動作 |
生成する |
auto |
親のスクロール内でのみstickyが動作 |
生成する |
clip |
影響なし(stickyが正常に動作) |
生成しない |
原因③:親要素の高さ不足
position: stickyは、親要素の範囲内でのみ固定されます。そのため、親要素の高さがsticky要素自体の高さとほぼ同じ場合、固定される余地がなく、stickyが効いていないように見えます。
なぜ親の高さが重要なのか
stickyの動作は以下の3ステップです。
sticky の動作サイクル
- ステップ1: 要素がスクロールで閾値に達する → 固定開始
- ステップ2: 親要素の範囲内で固定を維持
- ステップ3: 親要素の下端に達する → 固定解除(親と一緒にスクロールアウト)
親要素の高さが十分にないと、ステップ1の直後にステップ3が発生し、固定される時間がほぼゼロになります。
問題のあるコード
HTML
<div class="sidebar">
<div class="sticky-widget">
追従ウィジェット
</div>
</div>
CSS – NG: 親要素の高さが不十分
.sidebar {
/* 親要素にheight指定がない、またはコンテンツが少ない */
/* → sticky要素とほぼ同じ高さになる */
}
.sticky-widget {
position: sticky;
top: 0;
}
この場合、.sidebarの高さが.sticky-widgetとほぼ同じなので、固定される余地がありません。
修正方法:親要素に十分な高さを確保する
ブログのサイドバーなど、メインコンテンツと並ぶレイアウトでは、親要素がメインコンテンツと同じ高さになるようにします。
HTML – 2カラムレイアウト
<div class="layout">
<main class="main-content">
<!-- 長いコンテンツ -->
</main>
<aside class="sidebar">
<div class="sticky-widget">
追従ウィジェット
</div>
</aside>
</div>
CSS – OK: Flexboxで高さを揃える
.layout {
display: flex;
gap: 24px;
align-items: flex-start; /* 重要: stretch ではなく flex-start */
}
.main-content {
flex: 1;
min-height: 200vh; /* 長いコンテンツ */
}
.sidebar {
width: 300px;
align-self: stretch; /* メインコンテンツと同じ高さに */
}
.sticky-widget {
position: sticky;
top: 20px;
}
ポイント:stickyが効くには「親の高さ」>「sticky要素の高さ」である必要があります。Flexboxでalign-self: stretchを使ってサイドバーをメインコンテンツと同じ高さにすれば、stickyの固定範囲が確保されます。
height が明示的に指定されている場合
親要素にheightが明示的に指定されていて、その高さがsticky要素より少し大きい程度の場合も、stickyの固定時間はごくわずかになります。
CSS – NG: 親の height が小さすぎる
.parent {
height: 200px; /* 明示的な高さ制限 */
}
.sticky-child {
position: sticky;
top: 0;
height: 60px;
/* 親が200px、子が60pxなので140px分しか固定されない */
}
CSS – OK: height を削除して自然な高さに
.parent {
/* height を削除、またはmin-heightを使う */
min-height: 100vh;
}
.sticky-child {
position: sticky;
top: 0;
height: 60px;
}
min-height との関係
heightではなくmin-heightを使う方が、stickyとの相性が良い場合があります。
| プロパティ |
stickyとの相性 |
説明 |
height: auto(デフォルト) |
コンテンツ次第 |
子要素の合計高さになる |
height: 500px |
固定的 |
500px分だけstickyが動作 |
min-height: 100vh |
良好 |
最低でもビューポート1画面分の高さ |
height: 100% |
親次第 |
さらに上の親に依存する |
原因④:Flexbox / Grid レイアウトとの競合
モダンなレイアウトではFlexboxやCSS Gridを多用しますが、これらのレイアウトモデルにはposition: stickyと相性の悪いデフォルト値があります。
align-items: stretch による高さ引き伸ばし
Flexboxのalign-itemsのデフォルト値はstretchです。これにより、Flex子要素は親の高さいっぱいに引き伸ばされます。
サイドバーが引き伸ばされてメインコンテンツと同じ高さになると、sticky要素の親(サイドバー)とsticky要素の高さが同じになり、固定される余地がなくなる問題が発生します。
注意:前セクションの「親の高さ不足」と矛盾するように見えますが、ポイントは「sticky要素が親要素いっぱいに広がっている」かどうかです。親は十分な高さが必要ですが、sticky要素自体はコンパクトである必要があります。
HTML – 2カラムレイアウト
<div class="flex-layout">
<main class="main">
<!-- 長いメインコンテンツ -->
<p>Lorem ipsum...</p>
</main>
<aside class="sidebar">
<div class="sticky-widget">
追従ウィジェット
</div>
</aside>
</div>
CSS – NG: align-items のデフォルト(stretch)
.flex-layout {
display: flex;
gap: 24px;
/* align-items: stretch がデフォルト */
/* → .sidebar がメインコンテンツと同じ高さに引き伸ばされる */
/* → .sticky-widget は .sidebar いっぱいに広がる */
}
.sticky-widget {
position: sticky;
top: 20px;
/* stickyが効かないように見える */
}
解決方法1: サイドバーは stretch のまま、子要素に sticky
正しい方法は、サイドバーは stretch(高さ確保)のまま、sticky はウィジェットに適用することです。
CSS – OK: サイドバーは stretch + ウィジェットに sticky
.flex-layout {
display: flex;
gap: 24px;
align-items: stretch; /* サイドバーの高さ = メインの高さ */
}
.main {
flex: 1;
}
.sidebar {
width: 300px;
/* align-self はデフォルトの stretch → 親の高さが確保される */
}
.sticky-widget {
position: sticky;
top: 20px;
/* .sidebar は十分な高さがあるので sticky が正常動作 */
}
解決方法2: サイドバー自体を sticky にする
別のアプローチとして、align-items: flex-startにして、サイドバー自体をstickyにする方法もあります。
CSS – OK: サイドバー自体を sticky に
.flex-layout {
display: flex;
gap: 24px;
align-items: flex-start; /* stretch ではなく flex-start */
}
.sidebar {
position: sticky; /* サイドバー自体を sticky に */
top: 20px;
width: 300px;
}
Flexbox + sticky の正しい理解
- Flex親の
align-items: flex-start → 子要素は自身のコンテンツ高さになる
- Flex親の
align-items: stretch(デフォルト) → 子要素は親の高さいっぱいに伸びる
- サイドバーがstretchで伸びている場合、サイドバーの子要素にstickyを適用する
- サイドバーがflex-startで縮んでいる場合、サイドバー自体にstickyを適用する
CSS Grid での同様の問題
CSS Gridでもalign-itemsのデフォルトがstretchなので、同じ問題が発生します。
CSS – Grid レイアウトでの sticky
/* パターン1: Grid子要素自体に sticky */
.grid-layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 24px;
align-items: start; /* stretch ではなく start */
}
.sidebar {
position: sticky;
top: 20px;
}
/* パターン2: Grid子要素は stretch のまま、孫要素に sticky */
.grid-layout-v2 {
display: grid;
grid-template-columns: 1fr 300px;
gap: 24px;
/* align-items: stretch がデフォルト → サイドバーの高さが確保される */
}
.sidebar-widget {
position: sticky;
top: 20px;
}
align-items / align-self の各値とstickyの関係
| align-items の値 |
子要素の高さ |
stickyの適用先 |
stretch(デフォルト) |
親の高さに引き伸ばされる |
子要素の中の孫要素に適用 |
flex-start / start |
コンテンツの高さ |
子要素自体に適用可能 |
flex-end / end |
コンテンツの高さ |
子要素自体に適用可能 |
center |
コンテンツの高さ |
子要素自体に適用可能 |
Flexbox の flex-direction: column での sticky
flex-direction: columnのFlexboxでは、stickyの挙動が縦方向になります。
CSS – flex-direction: column での sticky
.vertical-layout {
display: flex;
flex-direction: column;
min-height: 200vh;
}
.sticky-section-header {
position: sticky;
top: 0;
background: #fff;
z-index: 10;
/* column方向でも top: 0 で正常に動作 */
}
原因⑤:display プロパティの問題
position: stickyは、すべてのdisplay値で動作するわけではありません。特定のdisplay値が設定されていると、stickyが無効になります。
display: inline では sticky が効かない
display: inline要素(<span>、<a>など)にはposition: stickyが効きません。インライン要素にはブロックボックスが生成されないためです。
CSS – NG: inline要素にsticky
span.sticky-label {
position: sticky;
top: 0;
/* span はデフォルトで display: inline */
/* → stickyが効かない */
}
CSS – OK: display: block に変更
span.sticky-label {
display: block; /* または inline-block */
position: sticky;
top: 0;
}
display: contents での問題
display: contentsは要素のボックスを生成しないため、position: stickyが効きません。
CSS – NG: display: contents
.wrapper {
display: contents; /* ボックスを生成しない */
}
.wrapper .sticky-child {
position: sticky;
top: 0;
/* .wrapper がボックスを持たないため、stickyの親要素が変わる */
}
display: table-cell での問題
display: table-cellの要素にposition: stickyを適用する場合は、テーブルレイアウトの制約を受けます。
CSS – table要素でのsticky
/* テーブルヘッダーの固定 */
thead th {
position: sticky;
top: 0;
background: #fff;
z-index: 1;
/* th(table-cell)でもstickyは動作する */
/* ただし、テーブル自体のoverflow設定に注意 */
}
stickyが動作する display 値の一覧
| display の値 |
sticky動作 |
備考 |
block |
動作する |
最も一般的 |
inline-block |
動作する |
インラインでもブロックボックスあり |
flex |
動作する |
Flex コンテナ自体にstickyを適用 |
grid |
動作する |
Gridコンテナ自体にstickyを適用 |
inline |
動作しない |
ブロックボックスがないため |
contents |
動作しない |
ボックスが生成されないため |
table-cell |
動作する |
th/td要素で使用可能 |
none |
表示されない |
要素自体が非表示 |
原因⑥:position プロパティの競合
position: stickyが他のCSSルールによって上書きされていると、stickyが効きません。
他のposition値で上書きされている
同じ要素に複数のCSSルールが適用されている場合、後から指定されたposition値が優先されます。
CSS – NG: position が上書きされている
/* ファイルA: sticky を指定 */
.header {
position: sticky;
top: 0;
}
/* ファイルB(後から読み込まれる): relative で上書き */
.header {
position: relative; /* sticky を上書き → stickyが効かない */
}
メディアクエリでの上書き
レスポンシブデザインで特定の画面幅のときにstickyが効かなくなるケースです。
CSS – NG: メディアクエリで上書き
.sidebar {
position: sticky;
top: 20px;
}
/* タブレット以下でstickyが解除されてしまう */
@media (max-width: 768px) {
.sidebar {
position: static; /* または relative */
}
}
意図的にモバイルでstickyを無効にする場合は問題ありませんが、意図せず上書きされている場合はメディアクエリを確認しましょう。
CSS – OK: 意図的なレスポンシブ対応
/* モバイルファーストアプローチ */
.sidebar {
position: static; /* モバイルでは通常配置 */
}
@media (min-width: 768px) {
.sidebar {
position: sticky; /* タブレット以上でsticky有効 */
top: 20px;
}
}
CSSの詳細度(Specificity)問題
セレクタの詳細度が高いルールがpositionを上書きしている可能性があります。
CSS – NG: 詳細度の高いセレクタが上書き
/* 詳細度: 0-1-0 */
.header {
position: sticky;
top: 0;
}
/* 詳細度: 0-2-0(こちらが優先される) */
.page-wrapper .header {
position: relative; /* sticky を上書き */
}
/* 詳細度: 1-0-0(IDセレクタはさらに高い) */
#site-header {
position: relative; /* sticky を上書き */
}
CSS – OK: 同じ詳細度以上で sticky を指定
/* 詳細度を合わせて上書き */
.page-wrapper .header {
position: sticky;
top: 0;
}
DevToolsで詳細度の問題を確認する
Chrome DevToolsの Elements タブで要素を選択し、Styles パネルを確認します。positionプロパティに取り消し線が引かれている場合、他のルールに上書きされています。
DevToolsでの確認手順
- F12でDevToolsを開く
- Elementsタブでsticky要素を選択
- 右側のStylesパネルで
positionを検索
- 取り消し線のある
position: stickyがあれば、上書きされている
- 上書きしているルールのセレクタと詳細度を確認
- Computedタブで最終的に適用されている
position値を確認
CSSフレームワークによる上書き
Bootstrap、Tailwind CSS、Foundation などのCSSフレームワークを使用している場合、フレームワークのスタイルがpositionを上書きしている可能性があります。
HTML – Bootstrapでの例
<!-- Bootstrap 5 の sticky-top ユーティリティ -->
<nav class="sticky-top">
ナビゲーション
</nav>
<!-- Tailwind CSS -->
<nav class="sticky top-0">
ナビゲーション
</nav>
ポイント:CSSフレームワークを使う場合は、フレームワークが提供するstickyユーティリティクラスを使うのが安全です。カスタムCSSと競合する場合は、DevToolsで詳細度を確認しましょう。
原因⑦:JavaScript による干渉
position: stickyはCSS単体で動作しますが、JavaScriptが意図せずstickyの動作を妨げるケースがあります。
scroll イベントでの style 変更
スクロールイベントで要素のpositionやtopをJavaScriptで動的に変更していると、stickyが正常に動作しません。
JavaScript – NG: scrollイベントでposition変更
// こうした古いスクロール固定のコードがstickyを妨げる
window.addEventListener('scroll', function() {
const header = document.querySelector('.header');
if (window.scrollY > 100) {
header.style.position = 'fixed'; // stickyを上書き
} else {
header.style.position = 'relative'; // stickyを上書き
}
});
CSS – OK: JavaScriptを削除してCSSだけで実現
/* JavaScriptでのスクロール制御は不要 */
/* position: sticky だけで同じことが実現できる */
.header {
position: sticky;
top: 0;
z-index: 100;
}
transform による新しいコンテナの生成
JavaScriptアニメーションライブラリ(GSAP、anime.js など)やCSSアニメーションでtransformが親要素に適用されると、新しいcontaining block(包含ブロック)が生成され、stickyの基準が変わる場合があります。
CSS – NG: 親要素に transform
.parent {
transform: translateZ(0); /* GPU高速化のためによく使われる */
/* これにより新しい包含ブロックが生成される */
}
.sticky-child {
position: sticky;
top: 0;
/* 期待通りに動作しない場合がある */
}
注意:transform、perspective、filter、will-changeなどのプロパティが親要素に設定されていると、新しい包含ブロックが生成されます。これはstickyの動作に影響を与える場合があります。ただし、多くのモダンブラウザではposition: stickyは包含ブロックではなくスクロールコンテナに基づくため、影響が出ないケースもあります。
IntersectionObserver との併用
IntersectionObserverはsticky要素の状態を検知するのに便利ですが、stickyの動作自体を妨げることはありません。むしろ、stickyと組み合わせて使うことが推奨されます。
JavaScript – stickyの固定状態を検知する
// stickyヘッダーが固定されたかどうかを検知
const header = document.querySelector('.sticky-header');
const sentinel = document.querySelector('.sticky-sentinel');
const observer = new IntersectionObserver(
([entry]) => {
header.classList.toggle('is-stuck', !entry.isIntersecting);
},
{ threshold: [1] }
);
observer.observe(sentinel);
CSS – 固定時にスタイルを変える
.sticky-header {
position: sticky;
top: 0;
transition: box-shadow 0.3s ease;
}
.sticky-header.is-stuck {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
ライブラリとの競合チェックリスト
| ライブラリ / パターン |
問題の内容 |
解決策 |
| jQuery scrollイベント |
positionの直接操作 |
JSを削除し、CSS stickyに置換 |
| GSAP ScrollTrigger |
transform / position の動的変更 |
pinType設定の確認 |
| smooth scroll ライブラリ |
スクロールコンテナの変更 |
ライブラリの設定でstickyサポートを確認 |
| WordPress プラグイン |
overflow / transformの自動付与 |
プラグインのCSSをDevToolsで確認 |
原因⑧:Safari / iOS 固有の問題
Safari(macOS / iOS)では、position: stickyに関する特有の問題がいくつかあります。
-webkit-sticky ベンダープレフィックス
Safari 13未満では、-webkit-stickyのベンダープレフィックスが必要でした。現在のSafari 13以降では不要ですが、古いiOSデバイスをサポートする場合はプレフィックスを追加しておくと安全です。
CSS – Safari対応(ベンダープレフィックス)
.sticky-header {
position: -webkit-sticky; /* Safari 6.1-12 対応 */
position: sticky;
top: 0;
}
ポイント:2025年以降、Safari 13以降のみをサポートするなら-webkit-stickyは不要です。しかし、念のため記述しても害はないので、プレフィックスを含めておくのが安全です。
iOS Safari のバウンススクロール問題
iOSのSafariでは、ページの上端や下端でバウンスする「ラバーバンドスクロール」があります。このバウンス中にsticky要素が一瞬ちらつく場合があります。
CSS – iOS バウンス対策
/* バウンス時のちらつきを軽減 */
.sticky-header {
position: -webkit-sticky;
position: sticky;
top: 0;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
Safari でのテーブル sticky の問題
Safariでは、<thead>や<tr>にposition: stickyを適用しても動作しません。<th>に直接適用する必要があります。
CSS – NG: thead に sticky(Safariで効かない)
/* Safariでは thead への sticky が効かない */
thead {
position: sticky;
top: 0;
}
CSS – OK: th に直接 sticky を適用
/* th に直接指定すればSafariでも動作する */
thead th {
position: -webkit-sticky;
position: sticky;
top: 0;
background: #fff;
z-index: 1;
}
Safari でのborderの問題(table sticky)
テーブルのヘッダーをstickyにした際、Safariではborder-collapse: collapseを使うと、スクロール時にボーダーが消える問題があります。
CSS – NG: border-collapse で border が消える
table {
border-collapse: collapse; /* Safariでsticky使用時にborderが消える */
}
thead th {
position: sticky;
top: 0;
border-bottom: 2px solid #333; /* スクロール時に消える */
}
CSS – OK: border-separate + box-shadow で代替
table {
border-collapse: separate; /* collapse ではなく separate */
border-spacing: 0; /* 隙間をなくす */
}
thead th {
position: sticky;
top: 0;
background: #fff;
/* border の代わりに box-shadow を使用 */
box-shadow: inset 0 -2px 0 #333;
}
Safari での overflow: clip サポート
Safari 16以降でoverflow: clipがサポートされています。古いSafariバージョンではoverflow: clipが認識されず、overflow: visible(デフォルト)として扱われます。この場合はstickyに影響しませんが、はみ出しの制御ができない点に注意が必要です。
| Safari の問題 |
影響範囲 |
解決策 |
| -webkit-sticky 必須 |
Safari 6.1-12 |
ベンダープレフィックス追加 |
| thead/tr にsticky効かない |
全バージョン |
th に直接適用 |
| border-collapse でborder消失 |
全バージョン |
border-separate + box-shadow |
| バウンススクロールでちらつき |
iOS Safari |
backface-visibility: hidden |
| overflow: clip 非対応 |
Safari 15以前 |
overflow: hidden を避ける設計 |
実装パターン集
ここでは、position: stickyの実践的な使用パターンを紹介します。各パターンで「正しく動作するための条件」も解説します。
パターン1:追従ヘッダー
最も一般的なstickyの使い方です。ページスクロール時にヘッダーが画面上部に固定されます。
HTML – 追従ヘッダー
<body>
<div class="top-bar">お知らせバー</div>
<header class="sticky-header">
<nav>
<a href="/">ホーム</a>
<a href="/about">概要</a>
<a href="/contact">お問い合わせ</a>
</nav>
</header>
<main>
<!-- メインコンテンツ -->
</main>
</body>
CSS – 追従ヘッダー
.sticky-header {
position: -webkit-sticky;
position: sticky;
top: 0;
background: #ffffff;
padding: 12px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.sticky-header nav {
display: flex;
gap: 24px;
align-items: center;
}
.sticky-header nav a {
text-decoration: none;
color: #333;
font-weight: 500;
}
ポイント:お知らせバーの下にヘッダーがある場合、スクロールするとお知らせバーがスクロールアウトし、ヘッダーだけがtop: 0に固定されます。z-indexを十分に大きくして、他の要素の上に表示されるようにしましょう。
パターン2:追従サイドバー(ブログ型レイアウト)
ブログでよく見る「メインコンテンツをスクロールしても、サイドバーのウィジェットが追従する」パターンです。
HTML – ブログ型レイアウト
<div class="blog-layout">
<main class="blog-main">
<article>
<h1>記事タイトル</h1>
<p>長い記事本文...</p>
<!-- 非常に長いコンテンツ -->
</article>
</main>
<aside class="blog-sidebar">
<div class="sidebar-widget">
<h3>人気記事</h3>
<ul>
<li><a href="#">記事1</a></li>
<li><a href="#">記事2</a></li>
</ul>
</div>
<div class="sidebar-sticky">
<h3>目次</h3>
<nav class="toc">...</nav>
</div>
</aside>
</div>
CSS – 追従サイドバー
.blog-layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 32px;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
align-items: start; /* 重要 */
}
.blog-sidebar {
display: flex;
flex-direction: column;
gap: 24px;
}
/* 固定しないウィジェット */
.sidebar-widget {
background: #f8fafc;
padding: 20px;
border-radius: 8px;
}
/* 追従するウィジェット */
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
top: 80px; /* ヘッダーの高さ分ずらす */
background: #f8fafc;
padding: 20px;
border-radius: 8px;
}
/* レスポンシブ対応 */
@media (max-width: 768px) {
.blog-layout {
grid-template-columns: 1fr;
}
.sidebar-sticky {
position: static; /* モバイルではsticky解除 */
}
}
注意:ここでのポイントはalign-items: startです。.blog-sidebar全体ではなく、その中の.sidebar-stickyにstickyを適用しています。align-items: startにより、サイドバーはコンテンツの高さに縮みますが、Grid親の.blog-layoutがメインコンテンツと同じ行にあるため、stickyの動作範囲は確保されます。
パターン3:テーブルのヘッダー固定
大きなデータテーブルをスクロールする際に、ヘッダー行を固定するパターンです。
HTML – テーブルヘッダー固定
<div class="table-container">
<table class="sticky-table">
<thead>
<tr>
<th>名前</th>
<th>メール</th>
<th>部署</th>
<th>ステータス</th>
</tr>
</thead>
<tbody>
<!-- 多くの行 -->
<tr><td>田中</td><td>tanaka@example.com</td><td>開発</td><td>在籍</td></tr>
<!-- ... 50行以上 ... -->
</tbody>
</table>
</div>
CSS – テーブルヘッダー固定(ページスクロール)
.sticky-table {
width: 100%;
border-collapse: separate; /* Safari対応 */
border-spacing: 0;
}
.sticky-table thead th {
position: -webkit-sticky;
position: sticky;
top: 0;
background: linear-gradient(135deg, #0284c7, #0369a1);
color: #fff;
padding: 12px 16px;
text-align: left;
font-weight: 600;
z-index: 1;
/* border の代わりに box-shadow(Safari対応) */
box-shadow: inset 0 -1px 0 #e2e8f0;
}
.sticky-table tbody td {
padding: 10px 16px;
border-bottom: 1px solid #e2e8f0;
}
.sticky-table tbody tr:nth-child(even) {
background: #f8fafc;
}
パターン4:スクロールコンテナ内でのテーブルヘッダー固定
テーブルを固定高さのコンテナ内でスクロールさせる場合のパターンです。
CSS – コンテナ内スクロール + ヘッダー固定
.table-container {
max-height: 400px;
overflow-y: auto; /* コンテナがスクロール */
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.sticky-table thead th {
position: sticky;
top: 0; /* コンテナの上端に固定 */
background: #0284c7;
color: #fff;
z-index: 1;
}
コンテナスクロールでの sticky の仕組み
.table-containerにoverflow-y: autoが設定されると、コンテナ自体がスクロールコンテナになる
- sticky要素は、このスクロールコンテナの中で固定される
top: 0はコンテナの上端を基準にする(ビューポートではない)
- ページ全体のスクロールではなく、コンテナ内のスクロールでstickyが動作する
パターン5:複数セクションの追従ナビ
各セクションにsticky見出しを設定し、スクロール中に現在のセクション見出しが表示されるパターンです。
HTML – セクション追従見出し
<div class="sections">
<section>
<h2 class="section-heading">セクション1: はじめに</h2>
<p>セクション1の内容...</p>
</section>
<section>
<h2 class="section-heading">セクション2: 基本概念</h2>
<p>セクション2の内容...</p>
</section>
<section>
<h2 class="section-heading">セクション3: 実践</h2>
<p>セクション3の内容...</p>
</section>
</div>
CSS – セクション追従見出し
.section-heading {
position: -webkit-sticky;
position: sticky;
top: 0;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
padding: 16px 24px;
margin: 0;
font-size: 1.2em;
color: #0369a1;
border-bottom: 2px solid #0284c7;
z-index: 10;
}
section {
padding: 24px;
min-height: 80vh; /* 各セクションに十分な高さ */
}
各<section>が親要素となるため、セクションが画面外にスクロールすると見出しも一緒に消え、次のセクションの見出しが表示されます。これはstickyが親要素の範囲内でのみ固定されるという特性を活かしたパターンです。
パターン6:フッター手前で止まるsticky
サイドバーがフッターに到達する前に固定を解除するパターンです。これはstickyの「親要素の範囲内でのみ固定」という特性を利用します。
HTML – フッター手前で止まる
<div class="page-layout">
<main>メインコンテンツ</main>
<aside class="sidebar">
<div class="sticky-toc">目次</div>
</aside>
</div>
<footer>フッター</footer>
CSS – フッター手前で止まる
.page-layout {
display: flex;
gap: 32px;
align-items: stretch; /* サイドバーの高さ = メインの高さ */
}
.sticky-toc {
position: sticky;
top: 20px;
/* .sidebar は .page-layout 内にあるため、
フッターに到達する前にstickyが解除される */
}
stickyは親要素(.sidebar)の範囲内でのみ固定されるため、.page-layoutの下端(= フッターの手前)で自然に固定が解除されます。特別なJavaScriptは不要です。
パターン7:横スクロールテーブルの列固定
横に長いテーブルで、最初の列を固定するパターンです。
CSS – 横スクロールテーブルの列固定
.horizontal-scroll-table {
overflow-x: auto;
max-width: 100%;
}
.horizontal-scroll-table table {
min-width: 800px;
border-collapse: separate;
border-spacing: 0;
}
/* 最初の列を固定 */
.horizontal-scroll-table td:first-child,
.horizontal-scroll-table th:first-child {
position: sticky;
left: 0; /* 横方向のsticky */
background: #fff;
z-index: 1;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
}
/* 左上のセルはヘッダー固定+列固定の両方 */
.horizontal-scroll-table thead th:first-child {
position: sticky;
top: 0;
left: 0;
z-index: 2; /* 他のstickyより上に */
}
DevTools でのデバッグ方法
position: stickyが効かないとき、Chrome DevToolsを使って原因を特定する方法を解説します。
ステップ1:要素の position 値を確認する
まず、sticky が実際に適用されているか確認します。
DevTools での確認手順
- F12キーでDevToolsを開く
- Elementsタブでsticky要素を選択(要素上で右クリック → 「検証」)
- 右側のStylesパネルで
position: stickyを検索
- 取り消し線があれば、他のCSSルールに上書きされている
- Computedタブに切り替え、
positionの値がstickyになっているか確認
ステップ2:overflow の祖先を確認する
DevToolsのConsoleに以下のコードを貼り付けて実行すると、問題のある祖先要素を特定できます。
JavaScript – overflow チェックスクリプト
// DevToolsのConsoleに貼り付けて実行
// sticky要素のセレクタを変更してください
(function checkStickyParents(selector) {
const el = document.querySelector(selector);
if (!el) {
console.error('要素が見つかりません:', selector);
return;
}
console.group('sticky デバッグ:', selector);
// 1. position の確認
const computed = getComputedStyle(el);
console.log('position:', computed.position);
console.log('top:', computed.top);
// 2. 親要素の overflow チェック
let parent = el.parentElement;
let issues = [];
while (parent) {
const s = getComputedStyle(parent);
if (s.overflow !== 'visible' &&
s.overflow !== 'clip') {
issues.push({
element: parent,
overflow: s.overflow,
overflowX: s.overflowX,
overflowY: s.overflowY
});
}
parent = parent.parentElement;
}
if (issues.length > 0) {
console.warn('overflow 問題のある祖先要素:');
issues.forEach(i =>
console.log(' ', i.element, 'overflow:', i.overflow)
);
} else {
console.log('overflow の問題なし');
}
// 3. 親要素の高さチェック
const parentEl = el.parentElement;
console.log('親の高さ:', parentEl.offsetHeight, 'px');
console.log('要素の高さ:', el.offsetHeight, 'px');
if (parentEl.offsetHeight <= el.offsetHeight) {
console.warn('親の高さが要素と同じか小さい → stickyが効かない可能性');
}
console.groupEnd();
})('.sticky-widget'); // ← セレクタを変更
ステップ3:Chrome の Position: sticky バッジ
Chrome 116以降では、DevToolsのElementsパネルでsticky要素に「position: sticky」バッジが表示されます。このバッジをクリックすると、stickyの固定範囲が視覚的にハイライトされます。
Chrome DevTools の sticky デバッグ機能
- Elements パネルで sticky 要素を選択
- 要素のタグ横に「sticky」バッジが表示される
- バッジをクリックすると、sticky の固定範囲(containing block)がハイライトされる
- 固定範囲が小さすぎる場合は、親要素の高さ不足が原因
ステップ4:Computed タブの活用
DevToolsのComputedタブでは、最終的に計算された値を確認できます。
| 確認項目 |
確認方法 |
期待値 |
| position |
Computed → position |
sticky |
| top / bottom |
Computed → top |
0px など数値 |
| display |
Computed → display |
block / flex など(inline以外) |
| 親の overflow |
親要素を選択 → Computed → overflow |
visible または clip |
| 親の height |
親要素を選択 → Layout セクション |
sticky要素より十分大きい |
よくあるデバッグのフローチャート
sticky が効かないときのデバッグ手順
- 1. Computed で
position が sticky か確認 → Noなら「原因⑥:position の競合」
- 2.
top/bottom が auto でないか確認 → autoなら「原因①:オフセット指定忘れ」
- 3.
display が inline でないか確認 → inlineなら「原因⑤:displayの問題」
- 4. 祖先の
overflow を確認 → visible/clip 以外なら「原因②:overflowの問題」
- 5. 親要素の高さを確認 → sticky要素と同じか小さいなら「原因③:高さ不足」
- 6. Flexbox/Gridの
align-items を確認 → 「原因④:Flexbox/Grid との競合」
まとめ
position: stickyが効かない原因は多岐にわたりますが、ほとんどの場合、以下の原因チェックリストで解決できます。
原因チェックリスト
| チェック項目 |
確認内容 |
解決策 |
| top / bottom の指定 |
top: 0 などのオフセット値が指定されているか |
top: 0 を追加 |
| 祖先の overflow |
祖先要素にoverflow: hidden/auto/scrollがないか |
overflow削除 or clip に変更 |
| 親要素の高さ |
親の高さ > sticky要素の高さ か |
親に十分な高さを確保 |
| Flexbox / Grid |
align-items: stretch で要素が引き伸ばされていないか |
align-items変更 or 孫要素にsticky |
| display の値 |
display: inline / contents でないか |
block / inline-block に変更 |
| position の上書き |
他のルールでpositionが上書きされていないか |
詳細度を調整 |
| JavaScript 干渉 |
JSがposition/styleを動的変更していないか |
JS削除 or CSS に移行 |
| Safari 対応 |
-webkit-sticky / th直接指定しているか |
ベンダープレフィックス追加 |
sticky vs fixed vs absolute 比較表
| 比較項目 |
sticky |
fixed |
absolute |
| 基準 |
スクロールコンテナ |
ビューポート |
positioned parent |
| フロー |
保持(スペース確保) |
離脱 |
離脱 |
| 固定タイミング |
閾値到達時 |
常時 |
常時(親基準) |
| 固定範囲 |
親要素の範囲内 |
常にビューポート |
親要素の範囲内 |
| JS不要 |
はい |
はい |
はい |
| 主な用途 |
セクションヘッダー、サイドバー |
グローバルヘッダー、FAB |
ドロップダウン、ツールチップ |
| レイアウト崩れ |
なし |
あり(要スペーサー) |
あり |
最終チェック:実装テンプレート
以下は、stickyを確実に動作させるための「安全なテンプレート」です。
CSS – 安全な sticky テンプレート
/* ✅ sticky を確実に動作させる条件 */
.sticky-element {
position: -webkit-sticky; /* 1. Safari対応 */
position: sticky; /* 2. sticky指定 */
top: 0; /* 3. オフセット値 */
z-index: 10; /* 4. 重なり順 */
background: #fff; /* 5. 背景色(下のコンテンツが透けないように) */
}
/* ✅ 親要素の条件 */
.sticky-parent {
/* overflow: visible(デフォルト)のまま */
/* 十分な高さを持つ(min-height 推奨) */
min-height: 100vh;
}
/* ✅ 祖先要素に overflow: hidden が必要な場合 */
.ancestor {
overflow: clip; /* hidden の代わりに clip を使用 */
}
position: stickyは非常に便利なCSSプロパティですが、その動作条件を正しく理解していないと「効かない」問題にハマりがちです。本記事で紹介した原因チェックリストとDevToolsデバッグ手順を活用して、sticky実装のトラブルを素早く解決してください。
ポイント:stickyが効かないときは、まずDevToolsでpositionの値とtopの値を確認し、次に祖先要素のoverflowをチェックしましょう。ほとんどの問題はこの2ステップで原因を特定できます。