CSS 固定ヘッダー 9選 — CSSだけ・JS不要
css 固定ヘッダーは、スクロールしても画面上部に留まり続けるナビゲーションをCSSの position:sticky で作る手法です。この記事では隠す・影を付ける・色を反転するなど9種類の動きを選び、判定の仕組みごとに難易度順で並べました。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 隠れてから再表示
- 02 スクロールで影が付く
- 03 色が反転する
- 04 ロゴだけ縮小
- 05 進捗バーを結合
- 06 サブナビが固定される
- 07 現在のセクション見出し固定
- 08 backdrop blur ヘッダー
- 09 スクロールスナップ連動ヘッダー
並び順は効果の派手さではなく、固定ヘッダーが「今どこにいるか」をどう判定するかという仕組みの難易度です。まずtransformだけで隠して戻す基本形から始め(01)、判定にIntersectionObserverのセンチネル要素を使う影付けと色反転を重ね(02〜03)、レイアウトを一切揺らさないロゴ縮小と、ヘッダー自身が進捗バーを兼ねる発展形へ進み(04〜05)、二段構えのサブナビと、通過中の見出しだけを差し替える発展的な判定を並べ(06〜07)、最後にbackdrop-filterとscroll-snapという実際の場面への転用で締めくくりました(08〜09)。04と05だけはIntersectionObserverを使わず、animation-timelineでスクロール量そのものをアニメーションのタイムラインとして扱うためJavaScriptを一行も書いていません。他の7つも判定にはscrollイベントかIntersectionObserverを使いますが、実際に動きを作っているのはCSSのtransitionとanimationだけです。
01隠れてから再表示
下にスクロールするとヘッダーがそのままの高さで上へ消え、上にスクロールした瞬間だけ戻ってきます。長い記事ページのヘッダーや、モバイルECの上部バーのように、コンテンツを長く隠したくない場面に向いています。
.head {
position: sticky;
top: 0;
transition: transform .3s ease-out;
}
.head.is-hidden {
transform: translateY(-100%);
}
02スクロールで影が付く
最上部では影がなく、少しでもスクロールするとヘッダーの下端にうっすら影が付いて本文との境界ができます。ドキュメントサイトやブログのヘッダー、薄い背景の上のナビによく合います。
.head__shadow {
position: absolute;
left: 0; right: 0; bottom: -48px;
height: 48px;
background: linear-gradient(rgba($stage-ink, .85), rgba($stage-ink, 0));
opacity: 0;
transition: opacity .2s ease-out;
}
.head.is-scrolled .head__shadow {
opacity: 1;
}
03色が反転する
暗いヒーロー画像の上では透明背景に白文字ですが、ヒーローを過ぎると背景と文字色がまるごと反転します。フルスクリーンのヒーローを持つLPや、ポートフォリオサイトの上部に向いています。
.head__layer {
position: absolute;
inset: 0;
}
.head__layer--dark { opacity: 1; transition: opacity .3s ease-out; }
.head__layer--light { background: $color; opacity: 0; transition: opacity .3s ease-out; }
.head.is-inverted .head__layer--dark { opacity: 0; }
.head.is-inverted .head__layer--light { opacity: 1; }
.head__logo {
position: relative;
color: $subject-cream;
}
04ロゴだけ縮小
ヘッダーの高さと背景はそのままに、ロゴマークだけが scale で縮み、スクロールしてもレイアウトが揺れません。ブランドロゴが大きいSaaSのLPや、レイアウトシフトを避けたいヘッダーに向いています。MDNのanimation-timeline対応表によると、animation-timelineはChrome 115以降とSafari 26以降で使え、Firefoxは安定版ではまだフラグの内側です。
.head__logo {
transform-origin: left center;
}
@supports (animation-timeline: scroll()) {
.head__logo {
animation: logo-shrink linear both;
animation-timeline: scroll(root);
animation-range: 0 150px;
}
}
@keyframes logo-shrink {
from { transform: scale(1); }
to { transform: scale(.72); }
}
05進捗バー結合
固定ヘッダーの下端に、読んだ分だけ伸びる細い進捗バーがヘッダーの子要素として結合し、ヘッダーと一体で動きます。長いブログ記事のヘッダーや、チュートリアル文書の上部に向いています。
.head__bar {
position: absolute;
left: 0; bottom: -3px;
width: 100%;
height: 3px;
transform: scaleX(0);
transform-origin: left;
}
@supports (animation-timeline: scroll()) {
.head__bar {
animation: fill-bar linear both;
animation-timeline: scroll(root);
}
}
@keyframes fill-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
06サブナビが固定される
メインヘッダーの下にあるカテゴリタブが、スクロールするとヘッダー直下に二段目として一緒に固定されます。ECのカテゴリタブや、ドキュメントサイトの目次タブに向いています。
.head {
position: sticky;
top: 0;
z-index: 2;
height: 56px;
}
.subnav {
position: sticky;
top: 56px;
z-index: 1;
border-bottom: 4px solid transparent;
transition: border-color .2s ease-out;
}
.subnav.is-stuck {
border-bottom-color: $subject-cream;
}
07現在のセクション見出し固定
長いリストをスクロールする間、ヘッダーの枠には今通過中の区間の見出しだけが表示され、次の区間に入った瞬間に入れ替わります。連絡先や用語集のようなアルファベット区分のリスト、長いFAQページに向いています。
.head__label {
grid-area: 1 / 1;
opacity: 0;
transform: translateY(8px);
transition: opacity .2s ease-out, transform .2s ease-out;
}
.head__label.is-current {
opacity: 1;
transform: translateY(0);
}
08backdrop blur ヘッダー
スクロールしても不透明にはならず、代わりに backdrop-filter のぼかしが有効になり、背後のコンテンツがぼんやり透けます。画像や動画の上に重ねるヘッダーや、iOS風アプリの上部バーに向いています。caniuseのbackdrop-filter対応表によると、backdrop-filterはChrome 76以降、Safari 18以降は接頭辞なしで使え、Firefoxは103以降で使えます。
.head {
background: rgba(255, 255, 255, .08);
backdrop-filter: blur(0);
transition: background .3s ease-out, backdrop-filter .3s ease-out;
}
.head.is-scrolled {
background: rgba(255, 255, 255, .55);
backdrop-filter: blur(10px);
}
09スクロールスナップ連動ヘッダー
本文がセクション単位でスクロールスナップする際、ヘッダー内のインジケーターが今止まったセクション番号に合わせて切り替わります。フルページのストーリーテリングサイトや、スライド式の製品紹介に向いています。
.stage {
scroll-snap-type: y mandatory;
}
.snap-section {
scroll-snap-align: start;
}
.head__dots i {
transform: scale(1);
transition: transform .2s ease-out, background .2s ease-out;
}
.head__dots i.is-current {
background: $color;
transform: scale(1.4);
}
どこで崩れるか — 落とし穴
9つとも判定は position の値そのものではなく、要素の交差やスクロール量から間接的に組み立てています。1つ目の落とし穴は04のロゴ縮小を測っているときに踏んだもので、animation-timeline: scroll(root) に対応していないブラウザでは @supports が丸ごと弾かれるため、ロゴが小さくならないだけでなく静止したまま等倍で残りました。05の進捗バーも同じ書き方をしているので、対応前のブラウザではバーが scaleX(0) のまま一度も伸びず、ヘッダー下端に何もない細い線が残ります。
2つ目は01の隠して見せる仕組みを組み立てたときのことです。ステージ全体に overflow: hidden をかけた状態のまま .head と最上位の祖先の間に別のスクロールコンテナを挟むと sticky が効かなくなり、ヘッダーはただ流れていく静止要素になってしまいました。祖先のどこにも overflow の挟まりがないことを確認してから、9つとも sticky が効くようになりました。修正済みの9ファイルを開くには u9rbcprm という文字列が必要です。下のカードからzipを取ってください。
3つ目は06と07のIntersectionObserverを組んだときのことです。06はrootMarginの上端をヘッダーの高さぶんだけ引いた -56px 0px 0px 0px にしないとサブナビが一段手前で「固定された」と誤判定し、07は逆に下端を -80% まで引かないと、リストの途中にいるだけで次の見出しに切り替わってしまいました。実際の数値はヘッダーの高さと検知したい範囲から逆算する必要があり、コピペだけでは合いませんでした。
アクセシビリティ
9つとも prefers-reduced-motion: reduce では transform や opacity の transition が止まり、ヘッダーは最終状態のまま残ります。隠れてから再表示(01)は常に表示された状態で止まり、色反転(03)は反転後の配色に固定されます。04のロゴ縮小と05の進捗バーは animation-timeline を使うスクロール連動アニメーションのため、transition ではなく animation プロパティ自体を止める必要があり、これを忘れると reduced-motion の設定でもスクロール量に合わせてロゴやバーが動き続けてしまいます。
@media (prefers-reduced-motion: reduce) {
.head, .head__shadow, .head__layer,
.head__logo, .head__bar, .subnav,
.head__label, .head__dots i { transition: none; animation: none !important; }
}
固定ヘッダー以外のCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。
FAQ
position: sticky と position: fixed は何が違いますか?
position: fixed はビューポート基準で常に同じ場所に留まりますが、position: sticky は親要素の範囲内でしか固定されず、親がスクロールし終えると一緒に流れていきます。この記事の9つは全て position: sticky を使っているため、ヘッダーの祖先に高さ制限があると途中で固定が外れることがあります。
Reactでは何を使ってヘッダーの状態を管理しますか?
zipのreact/フォルダの各コンポーネントは、01・02・03・06・07・08・09ではuseEffectの中でIntersectionObserverかscrollイベントを登録し、返ってきた真偽値をuseStateでクラス名に反映しています。04と05はCSSのanimation-timelineだけで動くため、state を持たずクラス名も固定のままです。
スマホでも同じように固定されますか?
はい、01から09まで、position自体はiOSとAndroidの主要ブラウザで同じように効きます。ただし04と05のanimation-timelineは対応前のブラウザでは動かないため、ロゴや進捗バーはスクロールしても静止したまま表示されます。