デモで学ぶ CSS 固定ヘッダー(position: fixed / sticky)
スクロールしても消えないヘッダーの作り方を、fixed と sticky の違い・コンテンツが隠れる問題・アンカーリンクのズレまでボタン操作で確認できます。すぐに使えるHTML・CSSのコードをコピーできます。
HTML・CSS・JSコードプレビューツール にて実装確認ができます。
① position: fixed — スクロールしても画面上部に残す
現在: 追従する(画面上部に固定)
<header class="site-header">
<div class="logo">ロゴ</div>
<nav>
<ul>
<li><a href="#">Menu 1</a></li>
<li><a href="#">Menu 2</a></li>
<li><a href="#">Menu 3</a></li>
</ul>
</nav>
</header>
.site-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: #1e293b;
color: #fff;
z-index: 100;
box-sizing: border-box;
}
.site-header nav ul {
display: flex;
gap: 16px;
list-style: none;
margin: 0;
padding: 0;
}
.site-header nav a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
font-size: 0.8rem;
}
position: fixed を指定した要素は、画面(ビューポート)を基準に位置が決まり、スクロールしても動きません。top: 0; left: 0; width: 100%; の3点セットで画面上端いっぱいに広げるのが定番です。他の要素に隠れないよう z-index も高めに設定しておきます。ページ全体の中でひとつだけ、常に見えていてほしい要素に使うプロパティです。
position: fixed は画面全体を基準にするため、記事の中に埋め込んだ枠内では動作を再現できません。そのため上のデモは枠内スクロールで同じ見え方になる position: sticky を使っています。実際のページに実装するときは、コピペ用コードのとおり position: fixed を使ってください。sticky そのものの使いどころは次の②で解説します。
② position: sticky — 途中まで流れて、そこから固定に切り替わる
現在: position: sticky; top: 0;
<div class="hero">メインビジュアル</div>
<header class="site-header">
<div class="logo">ロゴ</div>
<nav> ... </nav>
</header>
<main>
<p>コンテンツ</p>
</main>
.site-header {
position: sticky;
top: 0;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: #1e293b;
color: #fff;
z-index: 100;
box-sizing: border-box;
}
sticky は「普段は通常の要素として流れ、指定した位置まで来ると固定に切り替わる」という二段構えの挙動をします。top: 0 を書き忘れると固定に切り替わるタイミングが決まらず、まったく効かないので注意してください。top・right・bottom・left のうち最低ひとつは必須です。メインビジュアルの下にナビゲーションを置き、スクロールしたら画面上端に貼り付ける、という構成に向いています。
sticky の要素は通常のレイアウトの中に場所を確保したまま固定されるため、fixed のようにコンテンツが下に潜り込みません。次の③で解説する高さ合わせの余白が不要になるのが最大の利点です。一方で、sticky が効く範囲は親要素の中だけという制約があります。親に overflow: hidden や overflow: auto が付いていると効かなくなるほか、親要素を抜けると固定も解除されます。ページ全体で絶対に消したくないヘッダーは fixed、区切りごとに追従させたい見出しやサイドバーは sticky、と考えると選びやすくなります。
③ コンテンツが隠れる問題 — 高さぶんの余白を確保する
現在: スペーサー要素あり — 先頭行が見えています
<header class="site-header"> ... </header>
<div class="header-space"></div>
<main>
<p>コンテンツの先頭行</p>
</main>
.site-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
background: #1e293b;
z-index: 100;
}
.header-space {
height: 56px;
}
position: fixed を指定した要素は通常のレイアウトから外れ、後続の要素は「ヘッダーが無いもの」として上から詰めて配置されます。その結果、ページを開いた瞬間からコンテンツの先頭がヘッダーの下に潜り込みます。上のデモで「対策なし」を選ぶと、先頭行がヘッダーに隠れて読めなくなることが確認できます。
ヘッダーと同じ高さの空要素を直後に置く方法と、body(またはメインコンテンツ)に padding-top を与える方法があり、結果は同じです。CSSだけで完結する padding のほうが HTML が汚れず、あとから高さを変えるときも1か所の修正で済みます。ヘッダーの高さを –header-height のようなCSS変数にしておくと、ヘッダー本体と余白の数値がずれる事故を防げます。そもそも余白を用意したくない場合は、②の position: sticky を検討してください。
④ アンカーリンクの位置ズレ — scroll-margin-top で直す
現在: scroll-margin-top: 72px — 見出しが見えています
<header class="site-header"> ... </header>
<a href="#section-2">セクション2へ移動</a>
<h2 id="section-2">セクション2</h2>
<p>セクション2の本文です</p>
html {
scroll-behavior: smooth;
}
.site-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
z-index: 100;
}
h2[id] {
scroll-margin-top: 72px;
}
固定ヘッダーがあるページでアンカーリンクを踏むと、リンク先の見出しが画面のいちばん上に来ようとするため、ヘッダーの裏に潜り込んで見えなくなります。リンク先の要素に scroll-margin-top を指定すると、ジャンプ時にその分だけ手前で止まるようになります。値はヘッダーの高さ+少しの余裕(例: 56px + 16px = 72px)が目安です。JavaScript は不要で、CSS 1行で解決できます。
id を持つ見出しすべてにまとめて指定しておくのが安全です。h2[id], h3[id] { scroll-margin-top: 72px; } のように属性セレクタを使えば、見出しを追加するたびにCSSを書き足す必要がありません。あわせて html に scroll-behavior: smooth を指定すると、ジャンプが瞬間移動ではなくなめらかなスクロールになり、どこへ移動したのかが分かりやすくなります。
⑤ レスポンシブ対応 — PC / モバイルでメニューを切り替える
現在: PC表示(ナビメニュー表示中)
<header class="site-header">
<div class="logo">ロゴ</div>
<nav class="pc-menu">
<span>Menu 1</span>
<span>Menu 2</span>
<span>Menu 3</span>
</nav>
<div class="mobile-menu">
<div class="line"></div>
<div class="line"></div>
<div class="line"></div>
</div>
</header>
.site-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: #1e293b;
color: #fff;
z-index: 100;
box-sizing: border-box;
}
.pc-menu {
display: flex;
gap: 16px;
}
.pc-menu span {
color: rgba(255, 255, 255, 0.75);
font-size: 0.8rem;
}
.mobile-menu {
display: none;
flex-direction: column;
justify-content: space-between;
width: 22px;
height: 16px;
cursor: pointer;
}
.mobile-menu .line {
height: 2px;
background: rgba(255, 255, 255, 0.8);
border-radius: 2px;
}
@media (max-width: 800px) {
.pc-menu { display: none; }
.mobile-menu { display: flex; }
}
メディアクエリで画面幅に応じて表示を切り替えます。PCでは横並びのナビゲーション、狭い画面ではハンバーガーアイコン、というのが定番です。切り替えは display の none と flex を入れ替えるだけで、要素そのものを作り分ける必要はありません。アイコンをクリックしてメニューを開閉する動作は JavaScript の担当になります。
スマートフォンでは画面の高さが限られているため、ヘッダーが高いとコンテンツの表示領域を圧迫します。モバイルでは50px前後まで抑えるか、メディアクエリで高さを切り替えるのが実用的です。あわせて、余白の値とヘッダーの高さは必ず連動させてください。両方に直接数値を書くと、片方だけ変更したときにズレが発生します。–header-height のようなCSS変数を1か所に定義し、ヘッダーの height・余白・scroll-margin-top のすべてがそれを参照する形にしておくと安全です。
よくある質問
fixed と sticky はどちらを使うべきですか?
ページのどこにいても絶対に消したくないグローバルヘッダーなら fixed、「メインビジュアルの下に置いて、スクロールしたら上端に貼り付く」「セクションごとに見出しを追従させる」なら sticky が向いています。sticky はレイアウト上の場所を保持したまま固定されるため、③の余白対策が不要になるのが大きな利点です。ただし親要素の中でしか効かず、親に overflow: hidden が付いていると動かない点に注意してください。
position: fixed を指定したのに固定されません。
祖先要素のどれかに transform、filter、backdrop-filter、perspective、will-change のいずれかが指定されていないか確認してください。これらが指定された要素は fixed の位置基準(含みブロック)になるため、fixed 要素は画面ではなくその要素を基準に配置されます。アニメーション用に transform: translateZ(0) を付けた親が原因、というのがよくあるパターンです。ヘッダーを祖先の外(body直下)へ移動させれば解決します。
ヘッダーが他の要素の下に隠れてしまいます。
まず z-index を上げてみてください。z-index は position が static 以外の要素にしか効かないため、対象に position が指定されているかも確認が必要です。それでも解決しない場合は、隠している側の要素が新しいスタッキングコンテキストを作っている可能性があります。この場合、いくら数値を上げても親のコンテキストの外には出られないため、要素の入れ子構造そのものを見直す必要があります。z-index の値はサイト全体で 100 / 200 / 300 のように役割ごとに決めておくと衝突しにくくなります。
下にスクロールすると隠れて、上に戻すと現れるヘッダーを作りたいです。
スクロール方向の判定が必要なため、CSSだけでは実装できず JavaScript を併用します。直前のスクロール量と現在値を比較して方向を判定し、ヘッダーに transform: translateY(-100%) を付け外しする、という作りが一般的です。実装コードは固定配置ヘッダー(アニメーションあり)で解説しています。
ヘッダーの高さが画面幅によって変わる場合、余白はどうすればいいですか?
高さをCSS変数にまとめ、メディアクエリでは変数の値だけを切り替えるのが確実です。:root { --header-height: 56px; } と定義し、ヘッダーの height、余白の padding-top、scroll-margin-top のすべてを var(--header-height) で参照させておけば、切り替えは1か所で済みます。高さが内容によって変動して読めない場合は、そもそも余白計算が不要な position: sticky に切り替えるのが最も確実です。
当サイトで公開しているWebデザインやUIの実装例は、一覧として以下記事に纏めています。

