自分だけのブログを始めませんか?
WordPressブログなら、あなたのコーディング知識でデザインのカスタマイズが自由自在。
ブログデビューに最適な初心者向けサーバー環境を徹底比較しました。

WordPressブログなら、あなたのコーディング知識でデザインのカスタマイズが自由自在。
ブログデビューに最適な初心者向けサーバー環境を徹底比較しました。
CSSのposition: sticky;は、JavaScript不要でスクロールに応じた要素の追従・固定を実現するプロパティです。
本記事では、ヘッダーやサイドバー等の実装方法、テーブルやGridでの応用、「なぜか効かない」時の原因と解決策までを解説します。
また、CSSに関するカテゴリーページから学びたい内容を決めたい人は、以下のCSSページをご確認ください。
Webサイトを閲覧している際、下にスクロールしても追従してくる見出しや画面の端にくっついて離れないサイドバーを見たことはありませんか?
そういった「スクロールに連動した固定要素」をJavaScriptを使わずにCSSで実現するプロパティがposition: sticky;です。
ここでは、stickyプロパティの使い方、「fixedとの違い」や「効かない対策」を解説します。
position: sticky;は、一言で表すと「最初は通常通りスクロールされるが、指定した位置(閾値)に到達した瞬間に画面に固定される」というプロパティです。
例えばstickyとtopを指定すると、要素が画面の上端(top: 0)に到達するまでは普通にスクロールで上に移動しますが、上端に触れた瞬間に止まり画面に張り付いたままになります。
下端で固定したい場合はstickyとbottomを使用します。
追従要素を作る際は、「position: sticky;とtop(またはbottom、left、right)は、2つで1つのセットとして記述すること」です。
⭕️ 注意:ページ全体ではなく「下のグレーの枠の中」をスクロールしてください!
⚠️ この枠の中を下にスクロールしてください ↓
まだまだスクロール ↓
もう少し下へ ↓
見出しが枠の上端でピタッと止まりましたね!
この文章を読んでいる間も、上の見出しはずっとついてきます。
JavaScriptを使わなくても、CSSだけでこんなに簡単に実装できます。
どんどんスクロールしても、親要素の終わりが来るまでは固定され続けます。
(さらに下へ…)
(さらに下へ…)
(さらに下へ…)
(さらに下へ…)
(さらに下へ…)
🏁 スクロール枠の終わり
<div class="sticky-v2-wrapper">
<p class="sticky-v2-caption">⭕️ 注意:ページ全体ではなく「下のグレーの枠の中」をスクロールしてください!</p>
<div class="scroll-v2-container">
<div class="content-v2-section">
<p style="color:#dc3545; font-weight:bold;">⚠️ この枠の中を下にスクロールしてください ↓</p>
<p>まだまだスクロール ↓</p>
<p>もう少し下へ ↓</p>
</div>
<div class="sticky-v2-heading">
🌟 私は sticky 見出しです!(top: 0)
</div>
<div class="content-v2-section is-v2-long">
<p>見出しが枠の上端でピタッと止まりましたね!</p>
<p>この文章を読んでいる間も、上の見出しはずっとついてきます。</p>
<p>JavaScriptを使わなくても、CSSだけでこんなに簡単に実装できます。</p>
<p>どんどんスクロールしても、親要素の終わりが来るまでは固定され続けます。</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
<p>🏁 スクロール枠の終わり</p>
</div>
</div>
<div class="sticky-v2-code-area">
<span class="hl-comment">/* ❌ stickyだけ書いて満足してしまう */</span><br>
<span class="hl-blue">.title-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span> <span class="hl-comment">/* 🚨 topなどの座標がないと、スルーされてしまう! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ sticky と 座標(top等)は必ずセットで書く! */</span><br>
<span class="hl-blue">.title-success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">-webkit-sticky;</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 💡 画面の上端から0pxの位置でピタッと止める */</span><br>
}
</div>
</div>.sticky-v2-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.sticky-v2-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #dc3545;
text-align: center;
}
/* 💡 デモ用のスクロールエリア(親要素) */
.scroll-v2-container {
height: 350px !important; /* 💡 高さを固定してスクロールバーを出す */
overflow-y: auto !important; /* 💡 縦方向のスクロールを許可 */
background-color: #e9ecef !important;
border: 3px solid #adb5bd !important;
border-radius: 8px !important;
padding: 20px !important;
margin-bottom: 20px !important;
position: relative !important;
display: block !important;
}
/* 中のコンテンツ */
.content-v2-section {
padding: 20px !important;
background-color: #fff !important;
border-radius: 6px !important;
margin-bottom: 20px !important;
color: #333 !important;
font-size: 15px !important;
line-height: 1.8 !important;
border: 1px dashed #ccc !important;
}
.content-v2-section.is-v2-long {
height: 800px !important; /* スクロールさせるためにわざと極端に長くする */
}
/* =⭕️ 正解:stickyを使った見出し= */
.sticky-v2-heading {
/* 💡 stickyの必須プロパティ(!importantで強制適用) */
position: -webkit-sticky !important;
position: sticky !important;
top: 0 !important; /* 💡 上端に触れたら固定 */
z-index: 100 !important; /* 他の要素より手前に表示させる */
/* 装飾 */
background-color: #0d6efd !important;
color: #fff !important;
padding: 15px !important;
margin: 0 0 20px 0 !important;
border-radius: 6px !important;
font-size: 18px !important;
font-weight: bold !important;
text-align: center !important;
box-shadow: 0 4px 10px rgba(0,0,0,0.3) !important;
display: block !important;
}
/* =コード解説エリア= */
.sticky-v2-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}
.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }
.hl-comment { color: #6c757d; font-style: italic; }様々な要素で利用するpositionの使い方を詳しく知りたい人は「【CSS】positionの使い方とabsolute・fixed・relativeの使い分け」を一読ください。
stickyを学ぶ際、fixedやabsoluteといった他の配置プロパティとの違いを理解する必要があります。
fixed(絶対固定)absolute(絶対配置)relativeなど)を基準にして配置されます。sticky(粘着固定)fixedに似ていますが、決定的な違いは 「直近の親要素の範囲内でしか固定されない」 という点です。実務では、「ヘッダーのように全ページで常に追従させたいものはfixedを使い、サイドバーや目次のように『特定のエリアが終わったら一緒にスクロールして消えてほしい』ものにはstickyを使うこと」です。
効かない時は「親の高さ」と「親のoverflow」を疑うことです。
⭕️ stickyは「親要素の終わり」で固定が解除され、一緒に流れていく!
📦 stickyの親要素(ここが終わるまで固定)
↓ 親要素のスクロールエリア ↓
↓ 親要素のスクロールエリア ↓
↓ 親要素のスクロールエリア ↓
↓ 親要素のスクロールエリア ↓
↓ 親要素のスクロールエリア ↓
⚠️ ここで親要素が終わり!stickyも押し出される!
ここは親要素の外側です。
fixedはずっとついてきますが、stickyはもういません。
(さらに下へ…)
(さらに下へ…)
(さらに下へ…)
<div class="sticky-vs-wrapper">
<p class="sticky-vs-caption">⭕️ stickyは「親要素の終わり」で固定が解除され、一緒に流れていく!</p>
<div class="scroll-container-vs">
<div class="vs-box is-fixed">
🧲 fixed (ずっと固定)
</div>
<div class="sticky-parent-box">
<p class="parent-label">📦 stickyの親要素(ここが終わるまで固定)</p>
<div class="vs-box is-sticky">
🍯 sticky (親の枠内だけ)
</div>
<p class="filler-text">↓ 親要素のスクロールエリア ↓</p>
<p class="filler-text">↓ 親要素のスクロールエリア ↓</p>
<p class="filler-text">↓ 親要素のスクロールエリア ↓</p>
<p class="filler-text">↓ 親要素のスクロールエリア ↓</p>
<p class="filler-text">↓ 親要素のスクロールエリア ↓</p>
<p class="filler-text">⚠️ ここで親要素が終わり!stickyも押し出される!</p>
</div>
<div class="outside-box">
<p>ここは親要素の外側です。</p>
<p>fixedはずっとついてきますが、stickyはもういません。</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
<p>(さらに下へ...)</p>
</div>
</div>
<div class="sticky-vs-code-area">
<span class="hl-comment">/* 💡 fixed:親要素に関係なく、画面(ウィンドウ)にずっと固定 */</span><br>
<span class="hl-blue">.item-fixed</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">fixed;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">20px;</span><br>
}<br><br>
<span class="hl-comment">/* 💡 sticky:親要素の範囲内だけで固定。親が終わると一緒に消える */</span><br>
<span class="hl-blue">.item-sticky</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">20px;</span><br>
}
</div>
</div>.sticky-vs-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
position: relative;
}
.sticky-vs-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
/* スクロールエリア(擬似的なブラウザ画面) */
.scroll-container-vs {
height: 400px;
overflow-y: scroll;
background-color: #fff;
border: 4px solid #333;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
position: relative; /* fixedの擬似的な基準にするため */
}
/* 共通の箱 */
.vs-box {
width: 140px;
padding: 15px;
border-radius: 6px;
font-weight: bold;
color: #fff;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
z-index: 10;
}
/* =💡 fixed要素(画面に対する固定をシミュレート)= */
.is-fixed {
position: absolute; /* ※実際の仕様ではfixedですが、デモの枠内に収める都合上absoluteでシミュレートしてスクロールイベントで動かします(通常はposition: fixed;と書きます) */
top: 20px;
left: 20px;
background-color: #dc3545;
}
/* ※デモ用のハック:枠内fixedシミュレート */
.scroll-container-vs .is-fixed {
position: sticky;
top: 20px;
}
/* stickyの親要素 */
.sticky-parent-box {
background-color: #e2e3e5;
border: 2px dashed #adb5bd;
border-radius: 8px;
padding: 20px;
margin-left: 160px; /* fixedを避ける */
margin-bottom: 20px;
min-height: 500px; /* 💡 stickyが動くための高さをしっかり確保! */
}
.parent-label {
font-weight: bold;
color: #495057;
margin-top: 0;
}
.filler-text {
color: #6c757d;
margin: 40px 0;
}
/* =💡 sticky要素(親の中でだけ固定)= */
.is-sticky {
position: -webkit-sticky;
position: sticky;
top: 20px; /* 上から20pxで張り付く */
background-color: #0d6efd;
margin-bottom: 30px;
}
/* 親の外側エリア */
.outside-box {
background-color: #fff3cd;
padding: 20px;
border-radius: 8px;
margin-left: 160px;
height: 400px;
color: #856404;
}
/* =コード解説エリア= */
.sticky-vs-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}overflowの使い方を詳しく知りたい人は「【CSS】overflowの使い方とhiddenやscrollの使い分け」を一読ください。
Web制作の現場において、stickyが活躍するのは「ヘッダー」「フッター」「サイドバー(目次など)」というWebサイトの骨格となる3大パーツです。
ここでは、各パーツの実装方法と実務で直面する「パーツ特有の対策」を解説します。
Webサイトを下にスクロールしても、常に画面上部にメニューバーが張り付いているデザインは一般的です。
ヘッダーを上部固定する際は、「stickyを指定するヘッダーには、z-index: 100;などの『十分に高い重なり順』をセットで指定すること」です。
これを忘れるとレイアウト崩れを起こします。
⭕️ ヘッダーには必ず「高い z-index」をセットで指定しろ!
❌ 罠(z-indexなし)
下のコンテンツ(画像等)が上に被さる!
⭕️ 成功(z-indexあり)
ヘッダーが常に一番上に表示される!
<div class="st-head-wrapper">
<p class="st-head-caption">⭕️ ヘッダーには必ず「高い z-index」をセットで指定しろ!</p>
<div class="st-head-demo-area">
<div class="st-head-item">
<p class="st-head-label" style="color:#dc3545;">❌ 罠(z-indexなし)<br><small>下のコンテンツ(画像等)が上に被さる!</small></p>
<div class="scroll-head-box">
<div class="dummy-hero">ヒーロー画像</div>
<div class="st-navbar is-trap-nav">メニュー (z-indexなし)</div>
<div class="dummy-content is-relative-content">
画像や要素が<br>ヘッダーの上に<br>被さってしまう!
</div>
<div class="dummy-content" style="height: 300px;">(スクロール用)</div>
</div>
</div>
<div class="st-head-item">
<p class="st-head-label" style="color:#0d6efd;">⭕️ 成功(z-indexあり)<br><small>ヘッダーが常に一番上に表示される!</small></p>
<div class="scroll-head-box">
<div class="dummy-hero">ヒーロー画像</div>
<div class="st-navbar is-success-nav">メニュー (z-indexあり)</div>
<div class="dummy-content is-relative-content">
ヘッダーの下に<br>綺麗に潜り込む!
</div>
<div class="dummy-content" style="height: 300px;">(スクロール用)</div>
</div>
</div>
</div>
<div class="st-head-code-area">
<span class="hl-comment">/* ❌ stickyとtopしか書かない */</span><br>
<span class="hl-blue">.header-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 🚨 これだけだと、他の要素の下敷きになる危険性が高い! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ ヘッダーなら必ず z-index で最前面に出す! */</span><br>
<span class="hl-blue">.header-success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
<span class="hl-green">z-index:</span> <span class="hl-red">100;</span> <span class="hl-comment">/* 💡 これで絶対に下敷きにならない! */</span><br>
}
</div>
</div>.st-head-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.st-head-caption {
font-size: 14px; font-weight: bold; margin-bottom: 20px; color: #198754; text-align: center;
}
.st-head-demo-area {
display: flex; flex-wrap: wrap; gap: 30px; justify-content: center;
background-color: #e9ecef; padding: 30px 20px; border-radius: 8px; border: 1px dashed #adb5bd; margin-bottom: 20px;
}
.st-head-item { width: 250px; text-align: center; }
.st-head-label { font-size: 13px; font-weight: bold; margin-bottom: 10px; line-height: 1.5; color: #333; }
/* スクロールエリア */
.scroll-head-box {
height: 250px !important;
overflow-y: auto !important;
background-color: #fff !important;
border: 2px solid #adb5bd !important;
border-radius: 6px !important;
position: relative !important;
}
/* 擬似コンテンツ */
.dummy-hero { background-color: #e9ecef; padding: 30px; font-weight: bold; color: #6c757d; }
.dummy-content { padding: 20px; color: #333; border-bottom: 1px dashed #ccc; }
/* 画像などの相対配置要素をシミュレート */
.is-relative-content {
position: relative !important;
z-index: 10 !important;
background-color: #ffeb3b !important; /* 目立つように黄色 */
font-weight: bold !important;
}
/* 共通ナビゲーション */
.st-navbar {
background-color: #0d6efd !important;
color: #fff !important;
padding: 15px !important;
font-weight: bold !important;
position: sticky !important;
top: 0 !important; /* 上部固定 */
}
/* =❌ 罠:z-indexなし= */
.is-trap-nav {
z-index: auto !important; /* 🚨 下の黄色いコンテンツに負ける */
}
/* =⭕️ 成功:z-indexあり= */
.is-success-nav {
z-index: 100 !important; /* 💡 最前面を維持する */
box-shadow: 0 4px 6px rgba(0,0,0,0.2) !important;
}
/* コードエリア */
.st-head-code-area { background-color: #282c34; color: #abb2bf; padding: 20px; border-radius: 6px; font-family: monospace; font-size: 13px; line-height: 1.6; border-left: 4px solid #0d6efd; overflow-x: auto; text-align: left; }
.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }
.hl-comment { color: #6c757d; font-style: italic; }重なり順を決めるz-indexの使い方を詳しく知りたい人は「【CSS】z-indexとは?使い方と効かない時の対処」を一読ください。
スマホサイトで見かける「画面の下部に表示される購入ボタンやメニューバー」があります。
これもstickyを使うことで実装できます。
topが画面の上端を基準にするのに対し、bottom: 0;は「スクロール領域の下端」を基準にして固定されます。
コンテンツが長いページで、常にユーザーのアクションを促すフッターとして有効です。
画面下部に固定アクションバー(スマホ用メニューなど)を配置する際は、「大枠のfooter要素そのものにstickyをかけるのではなく、body全体を親要素とした『追従専用のバー』を独立して作り、position: sticky; bottom: 0;を指定すること」です。
※画面全体に対してずっと固定したいならposition: fixed; bottom: 0;の方が適している場合も多いので、親要素の終わりで消したいのか、ずっと出したいのかで使い分けましょう。
⭕️ 下部固定バーは「コンテンツの最後」に置くことで画面の底に張り付く!
下にスクロールしてください ↓
(長い記事コンテンツが続いています…)
画面に収まりきらないため、下部のアクションバーは「画面の底」に張り付いて追従してきます。
↓
↓
↓
コンテンツの終端が近づいてきました。
ここでstickyバーは本来の位置(親要素の終わり)に到達するため、追従をやめて一緒に上に流れていきます。
<div class="st-foot-v2-wrapper">
<p class="st-foot-v2-caption">⭕️ 下部固定バーは「コンテンツの最後」に置くことで画面の底に張り付く!</p>
<div class="scroll-foot-v2-box">
<div class="dummy-main-v2-content">
<p style="font-weight: bold; color: #dc3545;">下にスクロールしてください ↓</p>
<div class="long-spacer">
<p>(長い記事コンテンツが続いています...)</p>
<p>画面に収まりきらないため、下部のアクションバーは「画面の底」に張り付いて追従してきます。</p>
<p>↓</p>
<p>↓</p>
<p>↓</p>
<p>コンテンツの終端が近づいてきました。</p>
</div>
</div>
<div class="st-bottom-v2-bar">
🛒 カートに入れる (bottom: 0)
</div>
<div class="normal-v2-footer">
<h4>サイトの通常のフッター</h4>
<p>ここでstickyバーは本来の位置(親要素の終わり)に到達するため、追従をやめて一緒に上に流れていきます。</p>
</div>
</div>
<div class="st-foot-v2-code-area">
<span class="hl-comment">/* ⭕️ 画面下部に張り付かせるなら bottom: 0 を使う */</span><br>
<span class="hl-blue">.action-bar</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">-webkit-sticky;</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">bottom:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 💡 画面の下端に触れている間はピタッと止める */</span><br>
<span class="hl-green">z-index:</span> <span class="hl-red">100;</span> <span class="hl-comment">/* 💡 ヘッダー同様、z-indexは必須! */</span><br>
}
</div>
</div>.st-foot-v2-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.st-foot-v2-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
/* スクロールエリア */
.scroll-foot-v2-box {
height: 400px !important; /* 💡 デモ用のスクロール枠 */
overflow-y: auto !important;
background-color: #e9ecef !important;
border: 3px solid #adb5bd !important;
border-radius: 8px !important;
position: relative !important;
display: block !important;
}
/* メインコンテンツ */
.dummy-main-v2-content {
padding: 20px !important;
color: #333 !important;
background-color: #fff !important;
}
/* コンテンツを長引かせるためのスペーサー */
.long-spacer {
height: 500px !important; /* 💡 枠の高さ(400px)より長くして、バーを画面外に押しやる */
border-left: 3px dashed #ccc !important;
padding-left: 15px !important;
margin-top: 20px !important;
color: #666 !important;
}
/* =⭕️ 正解:下部固定アクションバー= */
.st-bottom-v2-bar {
/* stickyの必須設定 */
position: -webkit-sticky !important;
position: sticky !important;
bottom: 0 !important; /* 💡 画面の下端に固定 */
z-index: 100 !important;
/* 装飾 */
background-color: #dc3545 !important;
color: #fff !important;
padding: 20px !important;
font-size: 18px !important;
font-weight: bold !important;
text-align: center !important;
box-shadow: 0 -4px 10px rgba(0,0,0,0.2) !important;
display: block !important;
}
/* 通常のフッター */
.normal-v2-footer {
background-color: #343a40 !important;
color: #adb5bd !important;
padding: 60px 20px !important;
text-align: center !important;
display: block !important;
}
/* コードエリア */
.st-foot-v2-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
margin-top: 20px;
}
.hl-blue {
color: #61afef;
font-weight: bold;
}
.hl-red {
color: #e06c75;
font-weight: bold;
}
.hl-green {
color: #98c379;
font-weight: bold;
}
.hl-comment {
color: #6c757d;
font-style: italic;
}ブログ記事やメディアサイトで、左側の本文をスクロールしても右側のサイドバーにある「目次」や「広告」がずっとついてくるレイアウトです。
実現するには、display: flex;による2カラムレイアウトと組み合わせて使用されるのが王道パターンです。
Flexboxで作ったサイドバーをstickyで固定する際は、「サイドバーに対して、align-self: flex-start;を指定し、高さをコンテンツ本来のサイズに縮めること」です。
これで滑り降りる隙間が生まれ、追従し始めます。
⭕️ サイドバー固定が効かない?Flexboxの「stretch」を解除しろ!
❌ 罠(デフォルト/stretch)
サイドバーの高さが引き伸ばされ、固定されない
メインコンテンツ
↓ スクロール
⭕️ 成功(flex-startを指定)
高さが元に戻り、綺麗に追従し始める!
メインコンテンツ
↓ スクロール
<div class="st-side-wrapper">
<p class="st-side-caption">⭕️ サイドバー固定が効かない?Flexboxの「stretch」を解除しろ!</p>
<div class="st-side-demo-area">
<div class="st-side-item">
<p class="st-side-label" style="color:#dc3545;">❌ 罠(デフォルト/stretch)<br><small>サイドバーの高さが引き伸ばされ、固定されない</small></p>
<div class="scroll-side-box">
<div class="flex-container is-trap-flex">
<div class="main-content">
<p>メインコンテンツ</p>
<p>↓ スクロール</p>
<div style="height: 300px;"></div>
</div>
<div class="sidebar is-trap-sidebar">
❌ 追従しない!
</div>
</div>
</div>
</div>
<div class="st-side-item">
<p class="st-side-label" style="color:#0d6efd;">⭕️ 成功(flex-startを指定)<br><small>高さが元に戻り、綺麗に追従し始める!</small></p>
<div class="scroll-side-box">
<div class="flex-container is-success-flex">
<div class="main-content">
<p>メインコンテンツ</p>
<p>↓ スクロール</p>
<div style="height: 300px;"></div>
</div>
<div class="sidebar is-success-sidebar">
⭕️ 追従する!
</div>
</div>
</div>
</div>
</div>
<div class="st-side-code-area">
<span class="hl-comment">/* ❌ Flexboxのデフォルト仕様(stretch)に気づかない */</span><br>
<span class="hl-blue">.sidebar-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">20px;</span> <span class="hl-comment">/* 🚨 親と同じ高さに引き伸ばされているため、動けない! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ align-self: flex-start で自分の高さを縮める! */</span><br>
<span class="hl-blue">.sidebar-success</span> {<br>
<span class="hl-green">align-self:</span> <span class="hl-red">flex-start;</span> <span class="hl-comment">/* 💡 高さが元に戻り、滑り降りる隙間ができる */</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">20px;</span> <span class="hl-comment">/* 💡 これで完璧に追従する */</span><br>
}
</div>
</div>.st-side-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.st-side-caption {
font-size: 14px; font-weight: bold; margin-bottom: 20px; color: #198754; text-align: center;
}
.st-side-demo-area {
display: flex; flex-wrap: wrap; gap: 30px; justify-content: center;
background-color: #e9ecef; padding: 30px 20px; border-radius: 8px; border: 1px dashed #adb5bd; margin-bottom: 20px;
}
.st-side-item { width: 300px; text-align: center; }
.st-side-label { font-size: 13px; font-weight: bold; margin-bottom: 10px; line-height: 1.5; color: #333; }
/* スクロールエリア */
.scroll-side-box {
height: 300px !important;
overflow-y: auto !important;
background-color: #fff !important;
border: 2px solid #adb5bd !important;
border-radius: 6px !important;
text-align: left !important;
}
/* Flexコンテナ(2カラム) */
.flex-container {
display: flex !important;
gap: 15px !important;
padding: 15px !important;
/* デフォルトで align-items: stretch が効いている状態 */
}
/* メインコンテンツ(左側) */
.main-content {
flex: 1 !important;
background-color: #f1f3f5 !important;
padding: 15px !important;
border-radius: 6px !important;
border: 1px dashed #ccc !important;
}
/* 共通のサイドバー設定 */
.sidebar {
width: 100px !important;
background-color: #198754 !important;
color: #fff !important;
padding: 15px 10px !important;
border-radius: 6px !important;
font-weight: bold !important;
text-align: center !important;
/* stickyの必須設定 */
position: -webkit-sticky !important;
position: sticky !important;
top: 15px !important;
}
/* =❌ 罠:align-selfなし(メインと同じ高さに引き伸ばされる)= */
.is-trap-sidebar {
/* align-selfがないため、背景の緑色が下まで伸びきっており、動けない */
background-color: #dc3545 !important;
}
/* =⭕️ 成功:align-self: flex-start を指定= */
.is-success-sidebar {
align-self: flex-start !important; /* 💡 自分の本来の高さに縮むので、動ける! */
}
/* コードエリア */
.st-side-code-area { background-color: #282c34; color: #abb2bf; padding: 20px; border-radius: 6px; font-family: monospace; font-size: 13px; line-height: 1.6; border-left: 4px solid #0d6efd; overflow-x: auto; text-align: left; }Flexboxの設定について詳しく知りたい人は「【CSS】flexの使い方:justify-content・align-items・gap」を一読ください。
データ量が多い表(table)をスマホや小さな画面で見る時、スクロールすると「この列のデータ、何の項目だっけ?」と見出しが消えて迷子になってしまう問題はWebデザインにおける課題でした。
しかし、現在はこの問題もposition: sticky;を使うことで解決できます。
ここでは、テーブル特有のstickyの当て方、横スクロール時の列固定、実務で直面する「枠線が消える・背景が透ける」というバグの対処法を解説します。
thead(ヘッダー行)を縦スクロールしても残す方法border(枠線)が消える・透ける問題の対処法縦に長い表を下へスクロールした際、一番上の見出し行を常に画面上部に残す実装は、ユーザー体験(UX)を向上させます。
実装するには、ヘッダー要素に対してposition: sticky;とtop: 0;を指定するだけですが、テーブルタグ特有の「罠」が存在します。
テーブルのヘッダーを固定するには、「<tr>ではなく、その中にある見出しセル<th>要素に対して直接position: sticky; top: 0;を指定すること」です。
⭕️ ヘッダー固定は「tr」ではなく「th」に直接かけろ!
❌ 罠(trに指定)
ブラウザによっては無視され一緒に流れる
| ID | 名前 | 役職 |
|---|---|---|
| 1 | 田中 | 部長 |
| 2 | 佐藤 | 課長 |
| 3 | 鈴木 | 主任 |
| 4 | 高橋 | 一般 |
| 5 | 伊藤 | 一般 |
⭕️ 成功(thに指定)
どのブラウザでも綺麗に上部に固定される!
| ID | 名前 | 役職 |
|---|---|---|
| 1 | 田中 | 部長 |
| 2 | 佐藤 | 課長 |
| 3 | 鈴木 | 主任 |
| 4 | 高橋 | 一般 |
| 5 | 伊藤 | 一般 |
<div class="tbl-head-wrapper">
<p class="tbl-head-caption">⭕️ ヘッダー固定は「tr」ではなく「th」に直接かけろ!</p>
<div class="tbl-head-demo-area">
<div class="tbl-head-item">
<p class="tbl-head-label" style="color:#dc3545;">❌ 罠(trに指定)<br><small>ブラウザによっては無視され一緒に流れる</small></p>
<div class="tbl-scroll-box">
<table class="demo-table">
<tr class="is-trap-tr">
<th>ID</th>
<th>名前</th>
<th>役職</th>
</tr>
<tr><td>1</td><td>田中</td><td>部長</td></tr>
<tr><td>2</td><td>佐藤</td><td>課長</td></tr>
<tr><td>3</td><td>鈴木</td><td>主任</td></tr>
<tr><td>4</td><td>高橋</td><td>一般</td></tr>
<tr><td>5</td><td>伊藤</td><td>一般</td></tr>
</table>
</div>
</div>
<div class="tbl-head-item">
<p class="tbl-head-label" style="color:#0d6efd;">⭕️ 成功(thに指定)<br><small>どのブラウザでも綺麗に上部に固定される!</small></p>
<div class="tbl-scroll-box">
<table class="demo-table">
<tr>
<th class="is-success-th">ID</th>
<th class="is-success-th">名前</th>
<th class="is-success-th">役職</th>
</tr>
<tr><td>1</td><td>田中</td><td>部長</td></tr>
<tr><td>2</td><td>佐藤</td><td>課長</td></tr>
<tr><td>3</td><td>鈴木</td><td>主任</td></tr>
<tr><td>4</td><td>高橋</td><td>一般</td></tr>
<tr><td>5</td><td>伊藤</td><td>一般</td></tr>
</table>
</div>
</div>
</div>
<div class="tbl-head-code-area">
<span class="hl-comment">/* ❌ 行(tr)や thead を丸ごと固定しようとする */</span><br>
<span class="hl-blue">tr.row-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span> <span class="hl-comment">/* 🚨 テーブルの構造上、うまく効かないことが多い */</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ 個別のセル(th)に対して指定する! */</span><br>
<span class="hl-blue">th.cell-success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">-webkit-sticky;</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 💡 枠の上端でピタッと固定 */</span><br>
<span class="hl-green">z-index:</span> <span class="hl-red">1;</span> <span class="hl-comment">/* 💡 通常のセルより手前に出す */</span><br>
}
</div>
</div>.tbl-head-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.tbl-head-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.tbl-head-demo-area {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.tbl-head-item {
width: 250px;
text-align: center;
}
.tbl-head-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* スクロールする枠 */
.tbl-scroll-box {
height: 150px !important;
overflow-y: auto !important;
border: 2px solid #adb5bd !important;
border-radius: 6px !important;
background-color: #fff !important;
position: relative !important;
}
/* テーブル共通設定 */
.demo-table {
width: 100% !important;
border-collapse: collapse !important;
text-align: left !important;
font-size: 14px !important;
}
.demo-table th, .demo-table td {
padding: 10px !important;
border-bottom: 1px solid #dee2e6 !important;
}
.demo-table th {
background-color: #0d6efd !important;
color: #fff !important;
}
/* =❌ 罠:trにsticky= */
.is-trap-tr {
position: sticky !important;
top: 0 !important;
/* ※ブラウザによっては効かない。今回は「効かない場合」を想定したデモのため、他のthのstickyは外しています */
}
/* =⭕️ 成功:thにsticky= */
.is-success-th {
position: -webkit-sticky !important;
position: sticky !important;
top: 0 !important; /* 💡 固定位置 */
z-index: 1 !important;
/* 境界線を維持するためのハックは後述 */
}
/* コードエリア */
.tbl-head-code-area { background-color: #282c34; color: #abb2bf; padding: 20px; border-radius: 6px; font-family: monospace; font-size: 13px; line-height: 1.6; border-left: 4px solid #0d6efd; overflow-x: auto; text-align: left; }
.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }
.hl-comment { color: #6c757d; font-style: italic; }テーブルの作り方を詳しく知りたい人は「【HTML】tableタグ(テーブル)の使い方:枠線・セル結合・レスポンシブ」を一読ください。
スマホ対応において、横長の表を横スクロールさせるUIは必須です。
誰のデータか分かるように一番左の列を固定します。
また、複雑な表では2行目まで固定したい、2列目まで固定したいという要望も出ます。
複数列(または複数行)を固定するには、「2列目(2行目)以降は、手前の列(行)の『幅(高さ)』を計算し、その分だけ数値をズラしてleft: 100px;(またはtop: 50px;)のように指定すること」です。
⭕️ 複数列を固定する時は「手前の列の幅をガチガチに固定」して計算を合わせろ!
❌ 罠(すべて left: 0)
右にスクロールすると、1列目と2列目が重なって読めなくなる!
| ID | 名前 | 年齢 | 部署 | 電話番号 | メールアドレス | 保有資格 | 最終ログイン | 備考(ダミーテキストを長くして幅を広げています) |
|---|---|---|---|---|---|---|---|---|
| 1 | 田中 | 28 | 営業部 | 090-XXXX-XXXX | tanaka@example.com | ITパスポート | 2023/10/01 | あああああああああああああああああああああああああああ |
| 2 | 佐藤 | 35 | 開発部 | 080-XXXX-XXXX | sato@example.com | 基本情報技術者 | 2023/10/05 | いいいいいいいいいいいいいいいいいいいいいいいいいいい |
⭕️ 成功(幅を固定 & leftをズラす)
文字の重なりがなくなり、スクロールしても綺麗についてくる!
| ID | 名前 | 年齢 | 部署 | 電話番号 | メールアドレス | 保有資格 | 最終ログイン | 備考(ダミーテキストを長くして幅を広げています) |
|---|---|---|---|---|---|---|---|---|
| 1 | 田中 | 28 | 営業部 | 090-XXXX-XXXX | tanaka@example.com | ITパスポート | 2023/10/01 | あああああああああああああああああああああああああああ |
| 2 | 佐藤 | 35 | 開発部 | 080-XXXX-XXXX | sato@example.com | 基本情報技術者 | 2023/10/05 | いいいいいいいいいいいいいいいいいいいいいいいいいいい |
<div class="tbl-col-v3-wrapper">
<p class="tbl-col-v3-caption">⭕️ 複数列を固定する時は「手前の列の幅をガチガチに固定」して計算を合わせろ!</p>
<div class="tbl-col-v3-demo-area">
<div class="tbl-col-v3-item">
<p class="tbl-col-v3-label" style="color:#dc3545;">❌ 罠(すべて left: 0)<br><small>右にスクロールすると、1列目と2列目が重なって読めなくなる!</small></p>
<div class="tbl-x-scroll-v3-box">
<div class="scroll-v3-hint">👉 右へスクロールして違いを確認してください 👉</div>
<table class="demo-x-v3-table">
<tr>
<th class="trap-col-v3-1">ID</th>
<th class="trap-col-v3-2">名前</th>
<th>年齢</th><th>部署</th><th>電話番号</th><th>メールアドレス</th><th>保有資格</th><th>最終ログイン</th><th>備考(ダミーテキストを長くして幅を広げています)</th>
</tr>
<tr>
<td class="trap-col-v3-1">1</td>
<td class="trap-col-v3-2">田中</td>
<td>28</td><td>営業部</td><td>090-XXXX-XXXX</td><td>tanaka@example.com</td><td>ITパスポート</td><td>2023/10/01</td><td>あああああああああああああああああああああああああああ</td>
</tr>
<tr>
<td class="trap-col-v3-1">2</td>
<td class="trap-col-v3-2">佐藤</td>
<td>35</td><td>開発部</td><td>080-XXXX-XXXX</td><td>sato@example.com</td><td>基本情報技術者</td><td>2023/10/05</td><td>いいいいいいいいいいいいいいいいいいいいいいいいいいい</td>
</tr>
</table>
</div>
</div>
<div class="tbl-col-v3-item">
<p class="tbl-col-v3-label" style="color:#0d6efd;">⭕️ 成功(幅を固定 & leftをズラす)<br><small>文字の重なりがなくなり、スクロールしても綺麗についてくる!</small></p>
<div class="tbl-x-scroll-v3-box">
<div class="scroll-v3-hint" style="color: #0d6efd; border-color: #0d6efd;">👉 右へスクロールして違いを確認してください 👉</div>
<table class="demo-x-v3-table">
<tr>
<th class="success-col-v3-1">ID</th>
<th class="success-col-v3-2">名前</th>
<th>年齢</th><th>部署</th><th>電話番号</th><th>メールアドレス</th><th>保有資格</th><th>最終ログイン</th><th>備考(ダミーテキストを長くして幅を広げています)</th>
</tr>
<tr>
<td class="success-col-v3-1">1</td>
<td class="success-col-v3-2">田中</td>
<td>28</td><td>営業部</td><td>090-XXXX-XXXX</td><td>tanaka@example.com</td><td>ITパスポート</td><td>2023/10/01</td><td>あああああああああああああああああああああああああああ</td>
</tr>
<tr>
<td class="success-col-v3-1">2</td>
<td class="success-col-v3-2">佐藤</td>
<td>35</td><td>開発部</td><td>080-XXXX-XXXX</td><td>sato@example.com</td><td>基本情報技術者</td><td>2023/10/05</td><td>いいいいいいいいいいいいいいいいいいいいいいいいいいい</td>
</tr>
</table>
</div>
</div>
</div>
<div class="tbl-col-v3-code-area">
<span class="hl-comment">/* ❌ 固定したい列を全部 0 にしてしまう */</span><br>
<span class="hl-blue">.col-1, .col-2</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">left:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 🚨 スクロールすると2列目が1列目の上に覆いかぶさる! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ 1列目を60pxに固定したら、2列目は「left: 60px」にする! */</span><br>
<span class="hl-blue">.col-1</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">left:</span> <span class="hl-red">0;</span><br>
<span class="hl-green">width:</span> <span class="hl-red">60px;</span> <span class="hl-comment">/* 💡 幅を絶対に縮まないよう固定する */</span><br>
<span class="hl-green">box-sizing:</span> <span class="hl-red">border-box;</span><br>
}<br>
<span class="hl-blue">.col-2</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">left:</span> <span class="hl-red">60px;</span> <span class="hl-comment">/* 💡 1列目の幅の分だけ正確に右にズラす */</span><br>
}
</div>
</div>.tbl-col-v3-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.tbl-col-v3-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.tbl-col-v3-demo-area {
display: flex;
flex-direction: column;
gap: 30px;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.tbl-col-v3-item {
width: 100%;
text-align: left;
}
.tbl-col-v3-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* 横スクロールする枠 */
.tbl-x-scroll-v3-box {
width: 100% !important;
overflow-x: auto !important; /* 横スクロールを許可 */
border: 2px solid #adb5bd !important;
border-radius: 6px !important;
background-color: #fff !important;
position: relative !important;
}
/* スクロールを促すヒント表示 */
.scroll-v3-hint {
padding: 10px !important;
background-color: #fff !important;
color: #dc3545 !important;
font-weight: bold !important;
font-size: 13px !important;
text-align: center !important;
border-bottom: 2px dashed #dc3545 !important;
position: -webkit-sticky !important;
position: sticky !important;
left: 0 !important; /* スクロールしても常に左端に見えるようにする */
}
/* テーブル共通設定 */
.demo-x-v3-table {
width: 1500px !important; /* 💡 どんな大画面でも絶対にスクロールさせるために巨大化! */
min-width: 1500px !important;
border-collapse: collapse !important;
text-align: left !important;
font-size: 14px !important;
margin: 0 !important;
table-layout: fixed !important; /* セルの幅を厳格に守らせる */
}
.demo-x-v3-table th,
.demo-x-v3-table td {
padding: 15px 10px !important;
border-right: 1px solid #dee2e6 !important;
border-bottom: 1px solid #dee2e6 !important;
background-color: #fff !important;
box-sizing: border-box !important;
}
.demo-x-v3-table th {
background-color: #f1f3f5 !important;
}
/* =❌ 罠:すべて left: 0(スクロールで重なる)= */
.trap-col-v3-1 {
position: -webkit-sticky !important;
position: sticky !important;
left: 0 !important;
width: 60px !important;
background-color: #ffe6e6 !important;
z-index: 2 !important;
}
.trap-col-v3-2 {
position: -webkit-sticky !important;
position: sticky !important;
left: 0 !important; /* 🚨 ここがミス。1列目と同じ位置で固定しようとする */
width: 80px !important;
background-color: #e6f2ff !important;
z-index: 1 !important;
}
/* =⭕️ 成功:幅を固定し、2列目はズラす= */
.success-col-v3-1 {
position: -webkit-sticky !important;
position: sticky !important;
left: 0 !important;
width: 60px !important; /* 💡 幅を絶対に縮まないように固定 */
background-color: #ffe6e6 !important;
z-index: 2 !important;
}
.success-col-v3-2 {
position: -webkit-sticky !important;
position: sticky !important;
left: 60px !important; /* 💡 1列目の幅(60px)の分だけ正確にズラす */
width: 80px !important;
background-color: #e6f2ff !important;
z-index: 2 !important; /* 1列目と同じ階層で並べる */
}
/* コードエリア */
.tbl-col-v3-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}
.hl-blue {
color: #61afef;
font-weight: bold;
}
.hl-red {
color: #e06c75;
font-weight: bold;
}
.hl-green {
color: #98c379;
font-weight: bold;
}
.hl-comment {
color: #6c757d;
font-style: italic;
}テーブルのstickyにおいて、コーダーを苦しめてきたのがborderが消える仕様とスクロールした文字が透けてしまう問題です。
テーブルを固定する際は以下の2点です。
background-colorで不透明な色を指定すること。borderを使うのをやめ、box-shadowを使って枠線のように見える影を描画すること。⭕️ stickyのセルは「背景色」を塗り、「box-shadow」で枠線を引け!
❌ 罠(背景透明 & border使用)
文字が重なり、スクロールすると枠線が消える
| 見出し | 項目A |
|---|---|
| 裏の文字 | 裏の文字 |
| 裏の文字 | 裏の文字 |
| 裏の文字 | 裏の文字 |
⭕️ 成功(背景色あり & box-shadow使用)
文字は透けず、枠線もスクロールに耐える!
| 見出し | 項目A |
|---|---|
| 裏の文字 | 裏の文字 |
| 裏の文字 | 裏の文字 |
| 裏の文字 | 裏の文字 |
<div class="tbl-bug-wrapper">
<p class="tbl-bug-caption">⭕️ stickyのセルは「背景色」を塗り、「box-shadow」で枠線を引け!</p>
<div class="tbl-bug-demo-area">
<div class="tbl-bug-item">
<p class="tbl-bug-label" style="color:#dc3545;">❌ 罠(背景透明 & border使用)<br><small>文字が重なり、スクロールすると枠線が消える</small></p>
<div class="tbl-bug-scroll-box">
<table class="demo-bug-table is-collapse">
<tr>
<th class="is-trap-bug">見出し</th>
<th class="is-trap-bug">項目A</th>
</tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
</table>
</div>
</div>
<div class="tbl-bug-item">
<p class="tbl-bug-label" style="color:#0d6efd;">⭕️ 成功(背景色あり & box-shadow使用)<br><small>文字は透けず、枠線もスクロールに耐える!</small></p>
<div class="tbl-bug-scroll-box">
<table class="demo-bug-table is-collapse">
<tr>
<th class="is-success-bug">見出し</th>
<th class="is-success-bug">項目A</th>
</tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
<tr><td>裏の文字</td><td>裏の文字</td></tr>
</table>
</div>
</div>
</div>
<div class="tbl-bug-code-area">
<span class="hl-comment">/* ❌ 背景色を忘れ、通常の枠線を使ってしまう */</span><br>
<span class="hl-blue">th.trap</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
<span class="hl-comment">/* 🚨 背景がないので透ける。border-collapse下では border は消える */</span><br>
<span class="hl-green">border-bottom:</span> <span class="hl-red">1px solid #ccc;</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ 背景色を塗り、枠線は box-shadow で再現する! */</span><br>
<span class="hl-blue">th.success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
<span class="hl-green">background-color:</span> <span class="hl-red">#0d6efd;</span> <span class="hl-comment">/* 💡 背景を不透明にして透けを防止 */</span><br>
<span class="hl-green">box-shadow:</span> <span class="hl-red">0 1px 0 #333;</span> <span class="hl-comment">/* 💡 下向きに「1pxの影」を落として枠線を偽装する(絶対に消えない) */</span><br>
}
</div>
</div>.tbl-bug-wrapper { background-color: #f8f9fa; padding: 30px; border-radius: 8px; border: 1px solid #dee2e6; }
.tbl-bug-caption { font-size: 14px; font-weight: bold; margin-bottom: 20px; color: #198754; text-align: center; }
.tbl-bug-demo-area { display: flex; flex-wrap: wrap; gap: 30px; justify-content: center; background-color: #e9ecef; padding: 30px 20px; border-radius: 8px; border: 1px dashed #adb5bd; margin-bottom: 20px; }
.tbl-bug-item { width: 250px; text-align: center; }
.tbl-bug-label { font-size: 13px; font-weight: bold; color: #333; margin-bottom: 10px; line-height: 1.5; }
/* スクロールする枠 */
.tbl-bug-scroll-box {
height: 120px !important;
overflow-y: auto !important;
border: 2px solid #adb5bd !important;
border-radius: 6px !important;
background-color: #fff !important;
position: relative !important;
}
/* テーブル共通(border-collapseのバグを再現) */
.demo-bug-table {
width: 100% !important;
border-collapse: collapse !important; /* 🚨 これがあるとstickyのborderが消える */
text-align: center !important;
font-size: 14px !important;
}
.demo-bug-table td {
padding: 15px 10px !important;
border-bottom: 1px solid #dee2e6 !important;
color: #6c757d !important;
}
/* =❌ 罠:背景透明&通常border= */
.is-trap-bug {
position: sticky !important;
top: 0 !important;
padding: 10px !important;
color: #0d6efd !important;
font-weight: bold !important;
/* 背景色なし(透明) */
border-bottom: 2px solid #dc3545 !important; /* 🚨 スクロールすると消える */
}
/* =⭕️ 成功:背景色あり&box-shadow= */
.is-success-bug {
position: sticky !important;
top: 0 !important;
padding: 10px !important;
color: #fff !important;
font-weight: bold !important;
background-color: #0d6efd !important; /* 💡 背景を塗りつぶす */
/* border-bottom は使わない */
/* 💡 下向きにのみ1pxの影を落としてボーダーに見せる */
box-shadow: 0 2px 0 #0a58ca !important;
z-index: 1 !important;
}
/* コードエリア */
.tbl-bug-code-area { background-color: #282c34; color: #abb2bf; padding: 20px; border-radius: 6px; font-family: monospace; font-size: 13px; line-height: 1.6; border-left: 4px solid #0d6efd; overflow-x: auto; text-align: left; }影を実現するbox-shadowの使い方を詳しく知りたい人は「【CSS】box-shadowの使い方:おしゃれな影のコピペ集と浮遊感の作り方」を一読ください。
「CSSでposition: sticky;を指定したのに、stickyが効かない!」
「検索しても解決しない……」
position: sticky;は便利なプロパティですが、「CSSの中でもトップクラスに条件が厳しく、1つでもルールを破ると機能停止する」という厄介な特徴を持っています。
ここでは、実務でハマりやすい4つの原因と解決策(チェックリスト)を解説します。
overflow: hiddenがあるtop・bottomの指定忘れ/z-indexで裏に隠れているstickyが効かない原因の第1位が「親要素、あるいはさらに上の先祖要素のどこかにoverflow: hiddenが指定されている」という問題です。
CSSの仕様上、先祖要素にoverflowがかかっていると、新しい「スクロール領域の限界」として認識されてしまうため、画面全体に対するstickyが無効化されます。
効かない時は、「ブラウザの検証ツール(デベロッパーツール)を開き、sticky要素から<body>タグに至るまでのすべての親・先祖要素を辿り、overflowが隠れていないか調査すること」です。
⭕️ stickyの親や先祖に「overflow: hidden」があると絶対に動かない!
❌ 罠(親にoverflow: hidden)
stickyが完全に殺され、一緒に流れる
⭕️ 成功(overflow: visible)
制限から解放され、綺麗に追従する!
<div class="chk-ovf-wrapper">
<p class="chk-ovf-caption">⭕️ stickyの親や先祖に「overflow: hidden」があると絶対に動かない!</p>
<div class="chk-ovf-demo-area">
<div class="chk-ovf-item">
<p class="chk-ovf-label" style="color:#dc3545;">❌ 罠(親にoverflow: hidden)<br><small>stickyが完全に殺され、一緒に流れる</small></p>
<div class="scroll-ovf-box">
<div class="parent-trap-ovf">
<div class="st-ovf-box is-trap-ovf">
❌ 追従しない
</div>
<div class="dummy-text-ovf">親要素のコンテンツ</div>
<div class="dummy-text-ovf">スクロールしても...</div>
<div class="dummy-text-ovf">追従しません。</div>
<div class="dummy-text-ovf">下に流れます。</div>
</div>
</div>
</div>
<div class="chk-ovf-item">
<p class="chk-ovf-label" style="color:#0d6efd;">⭕️ 成功(overflow: visible)<br><small>制限から解放され、綺麗に追従する!</small></p>
<div class="scroll-ovf-box">
<div class="parent-success-ovf">
<div class="st-ovf-box is-success-ovf">
⭕️ 追従する!
</div>
<div class="dummy-text-ovf">親要素のコンテンツ</div>
<div class="dummy-text-ovf">スクロールすると...</div>
<div class="dummy-text-ovf">ピタッと!</div>
<div class="dummy-text-ovf">ついてきます。</div>
</div>
</div>
</div>
</div>
<div class="chk-ovf-code-area">
<span class="hl-comment">/* ❌ 親要素のどこかに overflow をかけてしまう */</span><br>
<span class="hl-blue">.parent-fail</span> {<br>
<span class="hl-green">overflow:</span> <span class="hl-red">hidden;</span> <span class="hl-comment">/* 🚨 どんなに深くても、先祖にコレがあると終了 */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ 親から body に至るまで、overflow は visible(初期値)にする! */</span><br>
<span class="hl-blue">.parent-success</span> {<br>
<span class="hl-green">overflow:</span> <span class="hl-red">visible;</span> <span class="hl-comment">/* 💡 これならstickyは正常に動く */</span><br>
}
</div>
</div>.chk-ovf-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.chk-ovf-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.chk-ovf-demo-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.chk-ovf-item {
width: 250px;
text-align: center;
}
.chk-ovf-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* デモ用のスクロール大枠 */
.scroll-ovf-box {
height: 200px;
overflow-y: auto;
background-color: #fff;
border: 2px solid #adb5bd;
border-radius: 6px;
position: relative;
text-align: left;
}
/* ダミーのテキスト */
.dummy-text-ovf {
padding: 20px;
color: #6c757d;
border-bottom: 1px dashed #ccc;
}
/* =❌ 罠:親にoverflow: hidden= */
.parent-trap-ovf {
background-color: #f8d7da;
padding: 10px;
overflow: hidden; /* 🚨 これがstickyを殺す元凶 */
}
.is-trap-ovf {
position: -webkit-sticky;
position: sticky;
top: 10px;
background-color: #dc3545;
color: #fff;
padding: 10px;
font-weight: bold;
text-align: center;
border-radius: 4px;
}
/* =⭕️ 成功:親が通常状態= */
.parent-success-ovf {
background-color: #d1e7dd;
padding: 10px;
overflow: visible; /* 💡 何も制限しない */
}
.is-success-ovf {
position: -webkit-sticky;
position: sticky;
top: 10px;
background-color: #0d6efd;
color: #fff;
padding: 10px;
font-weight: bold;
text-align: center;
border-radius: 4px;
}
/* =コード解説エリア= */
.chk-ovf-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}
.hl-blue {
color: #61afef;
font-weight: bold;
}
.hl-red {
color: #e06c75;
font-weight: bold;
}
.hl-green {
color: #98c379;
font-weight: bold;
}
.hl-comment {
color: #6c757d;
font-style: italic;
}次によくある原因が、「親要素の高さ」に関する問題です。
stickyは、「親要素の範囲内でのみ、滑り降りるように固定される」という仕様です。
つまり、親要素の中に「滑り降りるための余白(高さの余裕)」がないと、全く固定されません。
親要素の高さ不足を解決するには、「Flexboxの子要素にstickyを効かせる場合は、その要素自身にalign-self: flex-start;を指定して、高さを本来のサイズに縮めて隙間を作ること」です。
⭕️ stickyが動くには「親要素の中を滑り降りるための隙間」が必要!
❌ 罠(stretchで高さが同じ)
隙間がないため、その場で身動きが取れない
⭕️ 成功(flex-startで隙間を作る)
本来の高さに縮むことで、滑り降りる余裕ができる!
<div class="chk-hgt-wrapper">
<p class="chk-hgt-caption">⭕️ stickyが動くには「親要素の中を滑り降りるための隙間」が必要!</p>
<div class="chk-hgt-demo-area">
<div class="chk-hgt-item">
<p class="chk-hgt-label" style="color:#dc3545;">❌ 罠(stretchで高さが同じ)<br><small>隙間がないため、その場で身動きが取れない</small></p>
<div class="scroll-hgt-box">
<div class="flex-hgt-trap">
<div class="main-hgt-content">
メインが長い<br>↓<br>↓<br>↓<br>↓<br>終わり
</div>
<div class="st-hgt-sidebar is-trap-hgt">
❌ 隙間なし
</div>
</div>
</div>
</div>
<div class="chk-hgt-item">
<p class="chk-hgt-label" style="color:#0d6efd;">⭕️ 成功(flex-startで隙間を作る)<br><small>本来の高さに縮むことで、滑り降りる余裕ができる!</small></p>
<div class="scroll-hgt-box">
<div class="flex-hgt-success">
<div class="main-hgt-content">
メインが長い<br>↓<br>↓<br>↓<br>↓<br>終わり
</div>
<div class="st-hgt-sidebar is-success-hgt">
⭕️ 追従する!
</div>
</div>
</div>
</div>
</div>
<div class="chk-hgt-code-area">
<span class="hl-comment">/* ❌ Flexboxのデフォルト(stretch)をそのままにする */</span><br>
<span class="hl-blue">.sidebar-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">10px;</span> <span class="hl-comment">/* 🚨 親と同じ高さに引き伸ばされているため、動けない! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ align-self で自分の高さを縮め、隙間(余白)を作る! */</span><br>
<span class="hl-blue">.sidebar-success</span> {<br>
<span class="hl-green">align-self:</span> <span class="hl-red">flex-start;</span> <span class="hl-comment">/* 💡 高さが元に戻り、下へ滑り降りる余裕ができる */</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">10px;</span><br>
}
</div>
</div>.chk-hgt-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.chk-hgt-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.chk-hgt-demo-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.chk-hgt-item {
width: 250px;
text-align: center;
}
.chk-hgt-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* デモ用のスクロール大枠 */
.scroll-hgt-box {
height: 200px;
overflow-y: auto;
background-color: #fff;
border: 2px solid #adb5bd;
border-radius: 6px;
text-align: left;
}
/* メインコンテンツ(高さを稼ぐ役) */
.main-hgt-content {
flex-grow: 1;
background-color: #f1f3f5;
padding: 10px;
border: 1px dashed #ccc;
line-height: 2;
color: #555;
font-size: 12px;
}
/* 共通のサイドバースタイル */
.st-hgt-sidebar {
width: 90px;
padding: 10px;
color: #fff;
font-weight: bold;
text-align: center;
border-radius: 4px;
}
/* =❌ 罠:stretchで高さがパツパツ= */
.flex-hgt-trap {
display: flex;
gap: 10px;
padding: 10px;
/* align-items: stretch がデフォルトで効いている */
}
.is-trap-hgt {
position: -webkit-sticky;
position: sticky;
top: 10px;
background-color: #dc3545;
/* 🚨 高さがメインコンテンツと同じになってしまうため動けない */
}
/* =⭕️ 成功:flex-startで高さを縮める= */
.flex-hgt-success {
display: flex;
gap: 10px;
padding: 10px;
}
.is-success-hgt {
align-self: flex-start; /* 💡 自身の本来の高さに縮む */
position: -webkit-sticky;
position: sticky;
top: 10px;
background-color: #0d6efd;
}
/* =コード解説エリア= */
.chk-hgt-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}うっかりミスとして多いのが、「topなどの座標(閾値)の指定忘れ」と「z-indexの指定忘れによる裏被り」です。
position: sticky;だけを記述しても、「どこに張り付くか」が分からないため無効になります。
また、座標を書いて固定に成功しても、重なる問題が発生し、後から出てくる画像やテキストにヘッダーが下敷きにされてしまうことが多々あります。
追従要素を作った時は、「position: sticky;、top: 0;、z-index: 100;の3つは、セットとして記述すること」です。
⭕️ stickyには「座標(top)」と「z-index」が絶対に必要不可欠!
❌ 罠(z-indexなし)
下の画像(relative)が上に被さる!
⭕️ 成功(z-indexあり)
どんな要素が来ても最前面をキープ!
<div class="chk-z-wrapper">
<p class="chk-z-caption">⭕️ stickyには「座標(top)」と「z-index」が絶対に必要不可欠!</p>
<div class="chk-z-demo-area">
<div class="chk-z-item">
<p class="chk-z-label" style="color:#dc3545;">❌ 罠(z-indexなし)<br><small>下の画像(relative)が上に被さる!</small></p>
<div class="scroll-z-box">
<div class="st-z-header is-trap-z">
❌ 下敷きになるヘッダー
</div>
<div class="dummy-z-text">スクロールしてください</div>
<div class="dummy-z-text">↓</div>
<div class="dummy-z-img">
画像がヘッダーの上に乗る!
</div>
<div class="dummy-z-text">↓</div>
<div class="dummy-z-text">終わり</div>
</div>
</div>
<div class="chk-z-item">
<p class="chk-z-label" style="color:#0d6efd;">⭕️ 成功(z-indexあり)<br><small>どんな要素が来ても最前面をキープ!</small></p>
<div class="scroll-z-box">
<div class="st-z-header is-success-z">
⭕️ 最前面のヘッダー
</div>
<div class="dummy-z-text">スクロールしてください</div>
<div class="dummy-z-text">↓</div>
<div class="dummy-z-img">
画像はヘッダーの下に潜る!
</div>
<div class="dummy-z-text">↓</div>
<div class="dummy-z-text">終わり</div>
</div>
</div>
</div>
<div class="chk-z-code-area">
<span class="hl-comment">/* ❌ z-indexを書き忘れる */</span><br>
<span class="hl-blue">.header-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 🚨 これだけだと、relativeが指定された画像の裏に隠れる! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ sticky・top・z-index は三種の神器!必ずセットで書く! */</span><br>
<span class="hl-blue">.header-success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
<span class="hl-green">z-index:</span> <span class="hl-red">100;</span> <span class="hl-comment">/* 💡 これで絶対に下敷きにならない最前面の盾が完成する */</span><br>
}
</div>
</div>.chk-z-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.chk-z-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.chk-z-demo-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.chk-z-item {
width: 250px;
text-align: center;
}
.chk-z-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* デモ用のスクロール大枠 */
.scroll-z-box {
height: 200px;
overflow-y: auto;
background-color: #fff;
border: 2px solid #adb5bd;
border-radius: 6px;
position: relative;
text-align: center;
}
/* 共通のヘッダースタイル */
.st-z-header {
padding: 15px 10px;
color: #fff;
font-weight: bold;
}
/* ダミーのテキストと画像(relative) */
.dummy-z-text {
padding: 20px;
color: #6c757d;
}
.dummy-z-img {
position: relative; /* 🚨 重なりの原因となる相対配置 */
z-index: 10;
background-color: #ffc107;
color: #333;
font-weight: bold;
padding: 30px 10px;
border: 2px dashed #d39e00;
margin: 10px;
}
/* =❌ 罠:z-indexなし= */
.is-trap-z {
position: -webkit-sticky;
position: sticky;
top: 0;
background-color: #dc3545;
/* 🚨 z-indexがないため、下の画像の z-index:10 に負ける */
}
/* =⭕️ 成功:z-indexあり= */
.is-success-z {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 100; /* 💡 十分に高い数値を指定する */
background-color: #0d6efd;
box-shadow: 0 4px 6px rgba(0,0,0,0.2);
}
/* =コード解説エリア= */
.chk-z-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}チェックリストの最後は、ブラウザ間の互換性問題です。
「WindowsのChromeでは動いているのに、iPhoneで見たら全く固定されていない!」という悲劇は、ベンダープレフィックスの指定忘れが原因です。
古いiOSのSafariや一部のマイナーなブラウザ環境では、標準のposition: sticky;だけでは認識されません。
iPhone(Safari)への対応は、「先に-webkit-stickyを書き、直下の行に標準のstickyを書くこと」です。
これで、Safariは上の行を読み込み、モダンブラウザは下の標準仕様で上書きしてくれます。
⭕️ iPhone(Safari)で泣かないために、プレフィックスは必須!
❌ 罠(プレフィックスなし)
古いiPhone等で見た時に固定されない
⭕️ 成功(正しい順序で記述)
すべてのブラウザ・スマホで安全に稼働する
<div class="chk-safari-wrapper">
<p class="chk-safari-caption">⭕️ iPhone(Safari)で泣かないために、プレフィックスは必須!</p>
<div class="chk-safari-demo-area">
<div class="chk-safari-item">
<p class="chk-safari-label" style="color:#dc3545;">❌ 罠(プレフィックスなし)<br><small>古いiPhone等で見た時に固定されない</small></p>
<div class="safari-box is-trap-safari">
❌ position: sticky; のみ
</div>
</div>
<div class="chk-safari-item">
<p class="chk-safari-label" style="color:#0d6efd;">⭕️ 成功(正しい順序で記述)<br><small>すべてのブラウザ・スマホで安全に稼働する</small></p>
<div class="safari-box is-success-safari">
⭕️ -webkit- を先に書く
</div>
</div>
</div>
<div class="chk-safari-code-area">
<span class="hl-comment">/* ❌ Chromeでしか確認せず、プレフィックスを忘れる */</span><br>
<span class="hl-blue">.item-fail</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span> <span class="hl-comment">/* 🚨 古いiOS Safariでは無視されてしまう! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ 先に -webkit- を書き、次に標準仕様を書く! */</span><br>
<span class="hl-blue">.item-success</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">-webkit-sticky;</span> <span class="hl-comment">/* 💡 Safari用の指定(先) */</span><br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span> <span class="hl-comment">/* 💡 標準仕様(後。モダンブラウザはこっちで上書き) */</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
}
</div>
</div>.chk-safari-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.chk-safari-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.chk-safari-demo-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.chk-safari-item {
width: 250px;
text-align: center;
}
.chk-safari-label {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
line-height: 1.5;
}
/* 見た目のダミーボックス(動きではなく記述の啓蒙デモ) */
.safari-box {
padding: 20px 10px;
border-radius: 6px;
font-weight: bold;
color: #fff;
font-size: 14px;
}
/* =❌ 罠:プレフィックスなし= */
.is-trap-safari {
background-color: #dc3545;
border: 2px dashed #842029;
}
/* =⭕️ 成功:正しい順序= */
.is-success-safari {
background-color: #0d6efd;
border: 2px solid #084298;
}
/* =コード解説エリア= */
.chk-safari-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}基礎と各パーツごとの仕様をマスターしたら、実務で求められる高度なUI実装に入ります。
「ヘッダーの下にもう一つ別のメニューを固定したい」「固定された瞬間に影を出してフワッと浮かせたい」「Gridレイアウトの中で使いたい」といった複雑な要望が当たり前のように飛び交います。
ここでは、要素の高度な制御、JavaScriptと連携したスタイルの動的変更、Grid/Flexboxと組み合わせる際のテクニックを解説します。
sticky要素を段差で重ねるstickyの使い方「サイト全体のメインヘッダー」が上部(top: 0)に固定されている状態で、さらにスクロールすると「記事の目次メニュー」や「カテゴリタブ」がそのすぐ下に追従してくる…といった複数要素の固定は人気のあるUIです。
また、スクロールするたびにアルファベット順の見出し(A、B、C…)が次々と入れ替わるように固定される実装もこの応用になります。
複数のsticky要素を段差で重ねるには、「2つ目以降の要素のtop値は、上に固定されている要素の『高さ』の分だけ数値をズラして指定すること」です。
⭕️ 2つ目のstickyは、1つ目の「高さの分」だけtopをズラせ!
❌ 罠(すべて top: 0)
サブがメインの上に完全に覆いかぶさる
⭕️ 成功(top: 50px にズラす)
メインの下に綺麗にピタッと並んで固定!
<div class="stk-multi-wrapper">
<p class="stk-multi-caption">⭕️ 2つ目のstickyは、1つ目の「高さの分」だけtopをズラせ!</p>
<div class="stk-multi-demo-area">
<div class="stk-multi-item">
<p class="stk-multi-label trap-color">❌ 罠(すべて top: 0)<br><span class="label-small">サブがメインの上に完全に覆いかぶさる</span></p>
<div class="scroll-multi-box">
<div class="multi-main-header">
メインヘッダー
</div>
<div class="dummy-multi-space-small">スクロール ↓</div>
<div class="multi-sub-header is-trap-multi">
サブヘッダー (top: 0)
</div>
<div class="dummy-multi-space-large">(コンテンツ)</div>
</div>
</div>
<div class="stk-multi-item">
<p class="stk-multi-label success-color">⭕️ 成功(top: 50px にズラす)<br><span class="label-small">メインの下に綺麗にピタッと並んで固定!</span></p>
<div class="scroll-multi-box">
<div class="multi-main-header">
メインヘッダー
</div>
<div class="dummy-multi-space-small">スクロール ↓</div>
<div class="multi-sub-header is-success-multi">
サブヘッダー (top: 50px)
</div>
<div class="dummy-multi-space-large">(コンテンツ)</div>
</div>
</div>
</div>
<div class="stk-multi-code-area">
<span class="hl-comment">/* ❌ 全ての上部余白を 0 にしてしまう */</span><br>
<span class="hl-blue">.header-main, .header-sub</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span> <span class="hl-comment">/* 🚨 サブヘッダーがメインヘッダーに激突し、上に乗ってしまう! */</span><br>
}<br><br>
<span class="hl-comment">/* ⭕️ メインの高さ(50px)の分だけ、サブのtopをズラす! */</span><br>
<span class="hl-blue">.header-main</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">0;</span><br>
<span class="hl-green">height:</span> <span class="hl-red">50px;</span> <span class="hl-comment">/* 💡 高さをきっちり決める */</span><br>
}<br>
<span class="hl-blue">.header-sub</span> {<br>
<span class="hl-green">position:</span> <span class="hl-red">sticky;</span><br>
<span class="hl-green">top:</span> <span class="hl-red">50px;</span> <span class="hl-comment">/* 💡 50pxの位置から固定を開始させる */</span><br>
}
</div>
</div>.stk-multi-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.stk-multi-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.stk-multi-demo-area {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
.stk-multi-item {
width: 250px;
text-align: center;
}
.stk-multi-label {
font-size: 13px;
font-weight: bold;
margin-bottom: 10px;
line-height: 1.5;
}
.trap-color {
color: #dc3545;
}
.success-color {
color: #0d6efd;
}
.label-small {
font-size: 11px;
font-weight: normal;
color: #555;
}
/* スクロールエリア */
.scroll-multi-box {
height: 250px;
overflow-y: auto;
background-color: #fff;
border: 2px solid #adb5bd;
border-radius: 6px;
position: relative;
}
/* =💡 1つ目:メインヘッダー= */
.multi-main-header {
position: -webkit-sticky;
position: sticky;
top: 0;
height: 50px; /* 💡 高さを固定 */
background-color: #343a40;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
z-index: 10;
}
/* 共通のサブヘッダー */
.multi-sub-header {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
z-index: 9; /* メインより1つ下げる */
}
/* =❌ 罠:top:0= */
.is-trap-multi {
position: -webkit-sticky;
position: sticky;
top: 0; /* 🚨 メインヘッダーと同じ位置になるため覆いかぶさる */
background-color: #dc3545;
color: #fff;
}
/* =⭕️ 成功:top:50px= */
.is-success-multi {
position: -webkit-sticky;
position: sticky;
top: 50px; /* 💡 メインの高さ(50)の分だけ正確にズラす */
background-color: #0d6efd;
color: #fff;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* ダミーの空白要素 */
.dummy-multi-space-small {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
color: #6c757d;
background-color: #f8f9fa;
}
.dummy-multi-space-large {
height: 400px;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 20px;
color: #adb5bd;
}
/* コードエリア */
.stk-multi-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}ヘッダーがページの一番上にある時は背景と馴染ませておき、下にスクロールして 「画面に張り付いた瞬間にだけ、影をつけて浮き上がらせる」 という演出は、大手企業のサイトなどでよく使われます。
また、固定時に背景色を変えることで視認性を高めることもあります。
しかし、CSSのsticky単体には「自分が今固定されているかどうか」を判定する擬似クラス(例えば:stuckのようなもの)が存在しません。
そのため、状態を検知するにはJavaScriptとの連携が必須になります。
固定された瞬間を検知してスタイルを変えるには、「重いscrollイベントは使わず、代わりにIntersectionObserverAPIを使うことで、ヘッダーのすぐ上に『1pxの透明な監視用ダミー要素』を置き、それが画面から見えなくなった瞬間にクラスを付与すること」です。
⭕️ 重いスクロールイベントはNG!透明な監視役(Sentinel)を配置しろ!
↓ ゆっくり下にスクロール ↓
ヘッダーが上端に張り付くと…
JSが検知してクラスを付与し、
フワッと影(shadow)が出現します!
(さらに下へ)
(さらに下へ)
(さらに下へ)
<div class="stk-js-wrapper">
<p class="stk-js-caption">⭕️ 重いスクロールイベントはNG!透明な監視役(Sentinel)を配置しろ!</p>
<div class="stk-js-demo-area">
<div class="scroll-js-box">
<div id="js-sentinel" class="sentinel-element"></div>
<div id="js-sticky-header" class="dynamic-sticky-header">
スクロールすると影が出ます
</div>
<div class="dummy-js-content">
<p>↓ ゆっくり下にスクロール ↓</p>
<p>ヘッダーが上端に張り付くと...</p>
<p>JSが検知してクラスを付与し、</p>
<p>フワッと影(shadow)が出現します!</p>
<p>(さらに下へ)</p>
<p>(さらに下へ)</p>
<p>(さらに下へ)</p>
</div>
</div>
</div>
<div class="stk-js-code-area">
<span class="hl-comment"><!-- ⭕️ ヘッダーの直前に監視用の要素を置く --></span><br>
<span class="hl-blue"><div <span class="hl-green">class=</span><span class="hl-red">"sentinel"</span>></div></span><br>
<span class="hl-blue"><header <span class="hl-green">id=</span><span class="hl-red">"myHeader"</span>></span>ヘッダー<span class="hl-blue"></header></span><br><br>
<span class="hl-comment">// JS:IntersectionObserver で監視する(超軽量)</span><br>
<span class="hl-blue">const</span> <span class="hl-green">observer</span> = <span class="hl-blue">new</span> <span class="hl-green">IntersectionObserver</span>(<span class="hl-red">entries</span> => {<br>
<span class="hl-blue">if</span> (!entries[0].isIntersecting) {<br>
<span class="hl-comment">// 💡 監視役が画面外に出た = ヘッダーが固定された!</span><br>
header.classList.add(<span class="hl-red">'is-stuck'</span>);<br>
} <span class="hl-blue">else</span> {<br>
<span class="hl-comment">// 💡 監視役が画面に戻った = 固定解除!</span><br>
header.classList.remove(<span class="hl-red">'is-stuck'</span>);<br>
}<br>
});<br>
observer.observe(<span class="hl-green">document</span>.querySelector(<span class="hl-red">'.sentinel'</span>));
</div>
</div>.stk-js-wrapper {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.stk-js-caption {
font-size: 14px;
font-weight: bold;
margin-bottom: 20px;
color: #198754;
text-align: center;
}
.stk-js-demo-area {
display: flex;
justify-content: center;
background-color: #e9ecef;
padding: 30px 20px;
border-radius: 8px;
border: 1px dashed #adb5bd;
margin-bottom: 20px;
}
/* スクロールエリア */
.scroll-js-box {
width: 300px;
height: 250px;
overflow-y: auto;
background-color: #fff;
border: 2px solid #adb5bd;
border-radius: 6px;
position: relative;
}
/* 💡 監視用ダミー要素(見えないように配置) */
.sentinel-element {
width: 100%;
height: 1px;
background-color: transparent;
}
/* 💡 stickyヘッダーの初期状態 */
.dynamic-sticky-header {
position: -webkit-sticky;
position: sticky;
top: 0;
padding: 15px;
background-color: #e9ecef; /* 初期は薄いグレー */
color: #495057;
font-weight: bold;
text-align: center;
transition: all 0.3s ease; /* フワッと変化させるためのtransition */
z-index: 100;
box-shadow: 0 0 0 rgba(0,0,0,0); /* 初期は影なし */
}
/* 💡 JSによって付与されるクラス(固定された時のスタイル) */
.is-stuck-active {
background-color: #0d6efd; /* 固定時は青色に */
color: #fff;
/* 影を出して浮き上がらせる */
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
/* ダミーの空白要素 */
.dummy-js-content {
padding: 20px;
color: #6c757d;
height: 500px;
background-image: repeating-linear-gradient(0deg, transparent, transparent 19px, #f1f3f5 19px, #f1f3f5 20px);
}
/* コードエリア */
.stk-js-code-area {
background-color: #282c34;
color: #abb2bf;
padding: 20px;
border-radius: 6px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
border-left: 4px solid #0d6efd;
overflow-x: auto;
text-align: left;
}<script>
document.addEventListener("DOMContentLoaded", function() {
const sentinel = document.getElementById('js-sentinel');
const header = document.getElementById('js-sticky-header');
if (sentinel && header) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
// 画面外に出た=固定された
header.classList.add('is-stuck-active');
} else {
// 画面内に戻った=固定解除
header.classList.remove('is-stuck-active');
}
});
});
observer.observe(sentinel);
}
});
</script>分かりやすいようにまとめを記載します。
position: stickyは親要素の範囲内でのみ追従し、親の終端に到達すると固定が解除される。top、bottom、left、rightのいずれかの閾値(座標)指定が必須である。z-indexを併用する。align-self: start等を指定して要素の自動引き伸ばし(stretch)を解除しないと稼働しない。sticky要素を段差で重ねて固定する場合、手前の要素のサイズ(高さや幅)の分だけ座標数値をズラして指定する。width等で固定し、数値を次の列のleftに指定する。<tr>ではなく<th>などのセル要素へ直接指定する。box-shadowで代替して対応する。overflow: hidden(またはauto,clip)が存在すると、stickyは無効化される。position: sticky;の直前に-webkit-stickyを記述する。IntersectionObserverとダミー要素を用いたJS連携で検知する。実務で多い原因は、親要素やさらに上の先祖要素のどこかにoverflow: hidden(またはauto,clip)が指定されていることです。
CSSの仕様上、先祖にこれらの指定があるとstickyは機能停止します。
また、固定を開始する位置であるtop: 0;やbottom: 0;などの「座標指定」を書き忘れている場合もただの通常配置となってしまうため、まずはこの2点をデベロッパーツールで確認してください。
要素が固定される「範囲」が決定的に異なります。
fixedは「ブラウザの画面全体」を基準にして固定されるため、ページの一番下までずっとついてきます。
一方stickyは「直近の親要素の範囲内」でのみ固定されます。
そのため、親要素のコンテンツが終わると、固定が解除されて一緒に画面外へとスクロールされていくのが特徴です。
ヘッダーにはfixed、サイドバーや目次にはstickyが使われます。
z-indexの指定が不足しています。
ヘッダーの下にくるコンテンツにposition: relative;やアニメーションなどのスタイルがかかっていると、重なりの階層がヘッダーよりも上になってしまいます。
ヘッダーを上部固定する際は、position: sticky; top: 0; z-index: 100;のように十分大きなz-indexをセットで指定して最前面を維持してください。
テーブルのレンダリング仕様による問題です。
行要素である<tr>や<thead>に対して直接position: sticky;を指定しても、多くのブラウザではうまく固定されず一緒に流れてしまいます。
テーブルの見出しを固定したい場合は、行ではなく、その中にある見出しセル<th>要素それぞれに対して直接position: sticky;とtop: 0;を指定してください。
Safari向けの「ベンダープレフィックス」が不足しています。
一部のiOS Safariや古いブラウザ環境では、標準のプロパティ名だけでは認識されません。
解決するには、CSSの記述を以下のように2行に分けて書いてください。
position: -webkit-sticky; /* 先にSafari用を書く */
position: sticky; /* 次に標準仕様を書く */順番を逆にするとモダンブラウザで不具合が起きる可能性があるため、-webkit-を先に書くのがポイントです。
サイト制作でお困りの人はお気軽にご連絡ください。
どんなお悩み事も丁寧に返信させて頂きます。
「どのサーバーを選べばいいか分からない…」そんな悩みを解決!
WordPressデビューに最適なサーバーを徹底比較しました。
