GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

CSS スティッキーヘッダー追加9選 — 検索バー・CTA・通知まで

css スティッキーヘッダーとは、スクロールしてもページ上部のメニュー行が画面に貼り付いたまま残る機能で、position: stickyを一行書くだけで実現できます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9個は人気順ではなく、ヘッダーの中で何が変わるかで4つに分かれます。スペースを節約するためにどこかが折りたたまれたり狭まったりするグループ(01・02・07)、スクロール位置に合わせて案内情報が動いたり現れたりするグループ(03・06)、検索UIが広がったり別の行と合体したりするグループ(04・05)、そして画面になかった要素が新しく飛び出すグループ(08・09)です。

01お知らせバーの折りたたみ

バーが上へ消える

ページ最上部のプロモーションバーはスクロールを始めた瞬間、レイアウトを押し出さずに上へ消え、その空いた場所へロゴ行が上がってきます。

お知らせバーの折りたたみ
新しいタブで開く

01 · お知らせバーの折りたたみ — 計測 累積面積 23.972% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY(-100%)2단→1단scroll 임계값

レイアウトが揺れない理由

バーとヘッダーを同じスロットに絶対配置で重ねてtransformだけを動かすので、バーが消える間もレイアウトが揺れません。

.ahc__bar {
  position: absolute; inset: 0 0 auto 0;
  height: 32px;
  transform: translateY(0);
  opacity: 1;
  transition: transform .3s ease-out, opacity .3s ease-out;
}
.ahc__head {
  position: absolute; left: 0; right: 0; top: 32px;
  height: 56px;
  transform: translateY(0);
  transition: transform .3s ease-out;
}

// 実際の挙動: スクロールを始めるとバーが上へ消え、ヘッダーがその空いた場所まで上がる
.ahc.is-scrolled {
  .ahc__bar { transform: translateY(-100%); opacity: 0; }
  .ahc__head { transform: translateY(-32px); }
}

折りたたみのイージングは ease-out ではなく慣性 lerp カーブ — p += (目標 - p) * α を反復した結果を CSS linear() 関数に焼き込んでおり、スクロールを止めた後もヘッダーがわずかに追従して動きます。

02アイコンだけ残るナビ

文字ラベルが幅ごと消える

ナビゲーションリンクの文字ラベルはスクロールすると幅ごと消え、アイコンだけが残って狭くなったヘッダー幅を節約します。

アイコンだけ残るナビ
新しいタブで開く

02 · アイコンだけ残るナビ — 計測 累積面積 1.844% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

width:0 축소opacity 페이드clip 텍스트

max-widthも縮める必要

opacityだけを変えると余白がそのまま残ってしまうので、max-widthも一緒に0まで縮めて初めてアイコンが実際に左へ寄ります。

.ioc__label {
  overflow: hidden;
  white-space: nowrap;
  max-width: 40px;
  transform-origin: left center;
  transition: max-width .3s ease-out, transform .3s ease-out, opacity .3s ease-out;
}

// 実際の挙動: スクロールするとラベルが幅ごと折りたたまれ、アイコンだけが残る
.ioc.is-scrolled .ioc__label { max-width: 0; transform: scaleX(0); opacity: 0; }

03タブ下線のスクロール同期

スクロール 追従 ヘッダー の中で下線がタブを追う

ヘッダー内に並んだアンカータブの下で、下線ひとつが今通過中のセクションに合わせて隣のタブへ滑っていきます。

タブ下線のスクロール同期
新しいタブで開く

03 · タブ下線のスクロール同期 — 計測 累積面積 0.308% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateX 고정폭@keyframes tus-loopflex:0 0 auto

今のセクションを見つける方法

実際のページではIntersectionObserverで通過中のセクションを見つけ、そのタブの位置まで下線を移動させます。

// タブ幅がそれぞれ違うため offsetLeft・offsetWidth を実測して動かす — 当て推量のpx厳禁
var nav = document.getElementById('tusNav');
var tabs = nav.querySelectorAll('.tus__tab');
var underline = nav.querySelector('.tus__underline');
function moveTo(tab) {
  var w = underline.offsetWidth;
  underline.style.transform =
    'translateX(' + (tab.offsetLeft + tab.offsetWidth / 2 - w / 2) + 'px)';
}

04検索バーの吸収

検索行がヘッダーに合流する

ヘッダー下に別行で浮いていた検索入力欄が、スクロールすると上へスライドしてメインヘッダーの1行に合流します。

検索バーの吸収
新しいタブで開く

04 · 検索バーの吸収 — 計測 累積面積 15.596% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

2행→1행translateY 합치기높이 트랜지션

01番を逆方向に使う

2つの行を固定高さのスロットに重ねておき、検索行だけをヘッダーの高さぶんtranslateYで引き上げる仕組みで、01番と同じ層重ねの構造を逆方向に使っています。

.sbm__search {
  position: absolute; left: 0; right: 0; top: 56px;
  height: 40px;
  border-top: 1px solid rgba($stage-ink, .08);
  transform: translateY(0);
  transition: transform .3s ease-out;
}

// 実際の挙動: スクロールを始めると検索行が上へスライドしてヘッダー行の中に合流する
.sbm.is-scrolled .sbm__search { transform: translateY(-56px); }

05検索窓の拡張

アイコンが入力欄に広がる

虫眼鏡アイコンだけだった場所は、クリックすると幅が広がって実際の入力欄に変わります。

検索窓の拡張
新しいタブで開く

05 · 検索窓の拡張 — 計測 累積面積 3.736% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transition: width아이콘→입력창포커스 트리거

唯一スクロールと無関係

9個の中で唯一スクロールと無関係な項目で、トリガーはスクロール位置ではなくクリック(フォーカス)です。

.sse__search {
  width: 36px;
  border-radius: $r-pill;
  background: rgba($subject-cream, .12);
  overflow: hidden;
  transition: width .3s ease-spring, background .3s ease-spring;

  &.is-open { width: 156px; background: $subject-cream; }
}

06パンくずの表示

空いた場所にパスが現れる

ロゴ横の空いていた場所に、スクロールした瞬間に今見ている位置のパスがフェードとスライドで現れます。

パンくずの表示
新しいタブで開く

06 · パンくずの表示 — 計測 累積面積 13.451% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

opacity+translateX페이지 위치 표시순수 CSS

動きはCSSが担う

JavaScriptはクラスをひとつ付けるだけで、実際の動き(opacity・translateX)はすべてCSSのtransitionが担います。

.head__crumb {
  display: flex; align-items: center; gap: $sp-2;
  opacity: 0;
  transform: translateX(-16px);
  transition: opacity .2s ease-spring, transform .2s ease-spring;
}

// 実際の挙動: スクロールするとJSが付けたクラスがパンくずをフェード+スライドで見せる
.head.is-scrolled .head__crumb { opacity: 1; transform: translateX(0); }

07ユーザーメニューの圧縮

名前が折りたたまれる

右のアバター横に付いていたユーザー名の文字は、スクロールすると幅ごと折りたたまれてアバターアイコンだけが残ります。

ユーザーメニューの圧縮
新しいタブで開く

07 · ユーザーメニューの圧縮 — 計測 累積面積 0.298% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

max-width 축소텍스트 clip아바타만 유지

margin-rightも一緒に

02番(アイコンだけ残るナビ)と同じmax-width折りたたみ技法を右寄せ要素に適用した形で、margin-rightも一緒に0まで縮めて初めてアバターが実際に右端へ寄ります。

.head__name {
  display: inline-block;
  max-width: 104px;
  margin-right: $sp-2;
  overflow: hidden;
  white-space: nowrap;
  transform: scaleX(1);
  transform-origin: left;
  transition: max-width .3s ease-out, margin-right .3s ease-out,
              opacity .3s ease-out, transform .3s ease-out;
}

.head.is-scrolled .head__name {
  max-width: 0; margin-right: 0; opacity: 0; transform: scaleX(0);
}

08CTAボタンのスライドイン

一定距離で滑り込む

本文を一定の距離以上スクロールして初めて、ヘッダー右端に購入・問い合わせボタンが画面外から滑り込んできます。

CTAボタンのスライドイン
新しいタブで開く

08 · CTAボタンのスライドイン — 計測 累積面積 14.556% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateX(100%)→0스크롤 임계값transform만

しきい値が最大の理由

9個の中でスクロールのしきい値が最大(120px)で、ページに足を踏み入れた瞬間からボタンを押し付けません。

.head__cta {
  transform: translateX(140%);
  transition: transform .3s ease-spring;
}

// 実際の挙動: 120pxを超えてスクロールして初めてボタンが画面外から滑り込んでくる
.head.is-scrolled .head__cta { transform: translateX(0); }

09通知バッジの出現

点が弾けて脈打つ

スクロールが80pxを超えた瞬間、ヘッダーアイコンの上に通知の点が弾けるように現れ、その後は静かに脈打ちを繰り返します。

通知バッジの出現
新しいタブで開く

09 · 通知バッジの出現 — 計測 累積面積 0.449% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scale pop반복 pulsescroll 임계값

キーフレームを分けた理由

点が現れる瞬間用のキーフレームと、現れた後も回り続ける脈動用のキーフレームを分けてあるのが特徴です。

.head__dot {
  transform: scale(0);
  opacity: 0;
  transition: transform .15s ease-pop, opacity .15s ease-pop;
}

.head.is-scrolled .head__dot {
  transform: scale(1);
  opacity: 1;
  animation: dot-pulse-real 1.6s ease-in-out infinite;
}
@keyframes dot-pulse-real {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.3); opacity: .7; }
}

CSS スティッキーヘッダー、どこで壊れるか

当て推量のtranslateXは外れる

一番つまずきやすいのは03番のようなスライド式インジケーターです。下線やピルのようにタブの間を滑る要素は、タブの幅がそれぞれ違うと(このデモでは「概要」61.45px・「レポート」76.17px・「設定」61.45px)translateXの値を当て推量で入れたくなりますが、均等幅でない限りその推測は必ず外れます。この項目はflex: 0 0 autoでタブ幅を内容に合わせて固定したうえで、offsetLeft・offsetWidthを実測して下線の位置を計算しています(18.73px・87.54px・156.35px)。同じ間違いをCSSだけで再現してみると、タブにflex: 1 1 0(均等分配)を与えたまま固定幅の値をそのまま移すと、下線は短いタブでは届かず長いタブでははみ出します。03番のフォルダを直接開いてタブの文字数を変えてみれば、下線が実測値に沿って再計算されるかすぐに確認できます。

05番と重なり順

次に気をつけたいのは05番(検索窓の拡張)です。名前だけ見ると「スティッキーヘッダー」の仲間に見えますが、実際はスクロールと無関係にクリックだけで開くので、スクロールイベントリスナーを期待してコードを読むと見当たらず戸惑います。04番のように2つの層を重ねたスロット構造も、順番をひとつ間違えると(絶対配置レイヤーにz-indexを与えないと)検索行がロゴの上に来てしまうので、重なり順の確認が必要です — この9個のファイルをまとめたzipの合言葉はweqydbxaで、解凍すると同じvanilla/・react/構成で直接開いて見比べられます。

アクセシビリティ — 9個がreduced-motionで残すもの

時間だけをなくす

「動きを減らす」設定が有効な人に対して、この9個はそれぞれ少しずつ違う反応をします。01・02・04・06・07・08番はtransition: noneで切り替えの時間だけをなくし、実際のページでスクロールのしきい値を超えるとバーが折りたたまれ、ラベルが縮み、名前が消えるという結果自体はそのまま起こりますが瞬時に切り替わります — 機能は保ったままアニメーションだけを外した形です。

09番だけ扱いが違う

09番(通知バッジ)だけは違う扱いで、切り替えだけでなくスクロール後も繰り返し続くdot-pulse-realの脈動そのものを止めるので、点が一度現れたあとは完全に静止します — 終わりなく繰り返すアニメーションは切り替えとは別にそれ自体が動きなので、個別に止める必要があります。03番(タブ下線)と05番(検索窓)はそもそもスクロールに自動で反応しないので、減らす対象の「切り替え」もユーザーがクリックした瞬間にしかなく、その切り替えの時間だけをなくします。

関連リンク

スクロールに反応して中身が動くヘッダーをもっと見たい方はCSSスティッキーヘッダー9選でヘッダー自体が折りたたまれたり影が付いたりする基本形を扱っています。css ヘッダー 固定 スクロール に限らず、動く仕掛け全体はCSSスクロールアニメーション9選にまとめてあります。ほかのテーマはCSSカテゴリー一覧から探せます。position: stickyのスクロールコンテナに関する規則はMDNのリファレンスに、03番が使うビューポート検知はMDNのIntersectionObserver文書にまとまっています。

FAQ — よくある質問

css ヘッダー 固定 sticky の9個は全部スクロールしないと動きませんか?

いいえ。7個(01・02・04・06・07・08・09)はスクロール位置(20〜120pxのしきい値)に反応し、03番はどのセクションが見えているかに反応しますが、05番(検索窓の拡張)はスクロールと無関係にクリックでしか開きません。

ヘッダーが複数の層(バー+ヘッダー、ヘッダー+検索)で重なるとき、heightではなくtransformを使うのはなぜですか?

heightやmarginをアニメーションさせると、ブラウザが毎フレームレイアウトを計算し直すことになり、カクつきやすくなります。代わりに2つの層を絶対配置で重ねてtransform: translateY()だけを動かせば、レイアウト計算なしで合成レイヤーだけが変わるのでより滑らかです。01・04番がこの方式です。

vanillaとreactのコードは違いますか?

動き自体は同じです。違うのはトリガーだけで、vanillaのデモはis-demoクラスで絶え間なく繰り返し再生しますが、Reactコンポーネントは実際のscrollイベント(03番だけIntersectionObserver)を直接監視していて、条件が合ったときだけクラスを切り替えます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。