ヘッダー 中級

下スクロールで隠れるヘッダー

下方向スクロールでヘッダーを隠し、上方向に戻すと即座に再表示。読書領域を最大化しつつ操作性を保つ手法で、記事・ドキュメント・モバイルサイトに向きます。スクロール方向の差分で translateY を制御します。

#header#scroll#hide#navigation

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:読み物ページで下スクロール時にヘッダーを隠す -->
<div class="hos-frame">
  <header class="hos-head" id="hosHead">
    <a class="hos-logo" href="#" onclick="return false">☕ MOON BREW</a>
    <nav class="hos-nav">
      <a href="#" onclick="return false">メニュー</a>
      <a href="#" onclick="return false">物語</a>
      <a href="#" onclick="return false">店舗</a>
      <a href="#" onclick="return false">通販</a>
    </nav>
  </header>

  <div class="hos-scroll" id="hosScroll">
    <article class="hos-article">
      <h1>一杯のために、夜を焙煎する</h1>
      <p class="hos-lead">深夜の焙煎室から。MOON BREW の豆ができるまでの物語を、ゆっくりお届けします。読むあいだ、ヘッダーはそっと身を引きます。</p>
      <p>下方向に読み進めるとナビは隠れ、上に少し戻すと現れます。長い読み物でも本文に没入してもらえます。</p>
      <p>エチオピア・イルガチェフェの浅煎りは、ベリーのような明るい酸味。冷めていく過程で表情が変わるのも魅力です。</p>
      <p>抽出は92℃、30秒の蒸らしから。豆の状態に合わせて、毎朝レシピを微調整しています。</p>
      <p>このページは自動で下→上にスクロールし、ヘッダーの隠れる/現れるを実演します。</p>
      <p>次の章では、店舗での提供温度とカップの選び方についてお話しします。</p>
    </article>
  </div>
</div>
CSS
/* MOON BREW(カフェ):下スクロールで隠れるヘッダーの再スキン */
* { box-sizing: border-box; }
body { margin: 0; font-family: "Hiragino Mincho ProN", "Segoe UI", serif; }

.hos-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #f6f1e7; }

.hos-head {
  position: absolute; top: 0; left: 0; right: 0; z-index: 10;
  display: flex; align-items: center; gap: 18px; height: 56px; padding: 0 22px;
  background: #fffdf8; border-bottom: 1px solid #ece2d0;
  box-shadow: 0 4px 16px rgba(80,54,30,.07);
  transform: translateY(0); transition: transform .35s cubic-bezier(.4,0,.2,1);
}
.hos-head.is-hidden { transform: translateY(-100%); }
.hos-logo { font-family: "Segoe UI", system-ui, sans-serif; font-size: 17px; font-weight: 800; color: #7a4f2a; text-decoration: none; letter-spacing: .02em; }
.hos-nav { display: flex; gap: 4px; margin-left: auto; }
.hos-nav a { font-family: "Segoe UI", system-ui, sans-serif; color: #5b4630; text-decoration: none; font-size: 13px; font-weight: 600; padding: 7px 12px; border-radius: 8px; transition: background .2s ease, color .2s ease; }
.hos-nav a:hover { background: #f1e7d6; color: #7a4f2a; }

.hos-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; padding-top: 56px; }
.hos-article { max-width: 600px; margin: 0 auto; padding: 28px 26px 90px; color: #3c2f22; line-height: 1.95; }
.hos-article h1 { font-size: 27px; font-weight: 800; margin: 6px 0 16px; letter-spacing: .01em; }
.hos-lead {
  font-family: "Segoe UI", system-ui, sans-serif; font-size: 14.5px; color: #6a5742;
  padding: 14px 18px; margin: 0 0 22px; border-left: 4px solid #b07a3f;
  background: rgba(176,122,63,.08); border-radius: 0 10px 10px 0;
}
.hos-article p { margin: 0 0 18px; }

@media (prefers-reduced-motion: reduce) { .hos-head { transition: none; } }
JavaScript
// (デモと同じフックを流用)スクロール方向でヘッダーを隠す/見せる
(() => {
  const sc = document.getElementById('hosScroll');
  const head = document.getElementById('hosHead');
  if (!sc || !head) return;
  let last = 0; const DELTA = 4, TOP_LOCK = 60;
  function apply() {
    const y = sc.scrollTop;
    if (y < TOP_LOCK) head.classList.remove('is-hidden');
    else if (y - last > DELTA) head.classList.add('is-hidden');
    else if (last - y > DELTA) head.classList.remove('is-hidden');
    last = y;
  }
  let ticking = false;
  sc.addEventListener('scroll', () => {
    if (ticking) return; ticking = true;
    requestAnimationFrame(() => { apply(); ticking = false; });
  }, { passive: true });
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce, dir = 1;
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev => sc.addEventListener(ev, () => { auto = false; }, { passive: true }));
  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = sc.scrollHeight - sc.clientHeight;
      sc.scrollTop += dir * 2.2;
      if (sc.scrollTop >= Math.min(max, 240)) dir = -1; else if (sc.scrollTop <= 0) dir = 1;
      requestAnimationFrame(step);
    }, 1000);
  }
})();

実装ガイド

使いどころ

記事・ドキュメント・モバイルの縦長ページなど、本文の表示面積を最大化したいページに。下方向スクロールでヘッダーを隠し、上方向に少しでも戻すと即座に再表示します。

実装時の注意点

直前の scrollTop との差分でスクロール方向を判定し、translateY(-100%) を付け外しするだけの軽量実装です。小刻みな揺れを無視する DELTA と、最上部では必ず表示する TOP_LOCK でチラつきを抑えています。

対応ブラウザ

transform アニメーションと Scroll イベントは全モダンブラウザで安定動作します。passive リスナーでスクロール性能を阻害しません。特別なフォールバックは不要です。

よくある失敗

差分判定にしきい値を設けないと、慣性スクロールの微振動でヘッダーが点滅します。最上部付近で隠したままになると到達不能になるため TOP_LOCK は必須。固定要素なので、本文側に同じ高さの padding-top を必ず確保してください。

応用例

隠す代わりに半透明+縮小に変える、上スクロール時だけ検索バーを出す、一定速度以上の素早いスワイプでのみ反応させる、といった調整が可能です。

コード

HTML
<!-- 下スクロールで隠れ、上スクロールで戻るヘッダー -->
<div class="hos-frame">
  <header class="hos-head" id="hosHead">
    <a class="hos-logo" href="#" onclick="return false">◢ Lumen</a>
    <nav class="hos-nav">
      <a href="#" onclick="return false">ホーム</a>
      <a href="#" onclick="return false">特集</a>
      <a href="#" onclick="return false">連載</a>
      <a href="#" onclick="return false">購読</a>
    </nav>
  </header>

  <div class="hos-scroll" id="hosScroll">
    <article class="hos-article">
      <h1>読むときは消え、戻すと現れる</h1>
      <p class="hos-lead">下方向のスクロールではヘッダーを隠して本文に集中させ、上方向に少しでも戻すと即座に再表示します。スクロール方向の差分を見て translateY を切り替えるだけの軽量実装です。</p>
      <p>ニュース記事やドキュメント、モバイルの縦長ページで特に有効。常時固定だと狭い画面で本文を圧迫しますが、この方式なら必要な瞬間だけナビが戻ってきます。</p>
      <p>判定はシンプル。直前の scrollTop と比較して、増えていれば隠し、減っていれば見せる。最上部付近では必ず表示してチラつきを防ぎます。</p>
      <p>閾値とアニメーション速度はCSSとJSの定数だけで調整可能。プロジェクトのトーンに合わせて軽快にも上品にもできます。</p>
      <p>このデモは一度だけ自動で下→上へスクロールし、隠れる/現れるの両方を見せます。ホイールやタッチで触れると自動は停止します。</p>
      <p>最後の段落。スクロール方向ベースのヘッダー制御は、固定ヘッダーの「邪魔さ」を消しつつ到達性を残す折衷案として定番です。</p>
    </article>
  </div>
</div>
CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.hos-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #f6f7fb; }

.hos-head {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 10;
  display: flex; align-items: center; gap: 18px;
  height: 56px; padding: 0 22px;
  background: #ffffff;
  border-bottom: 1px solid #eceef3;
  box-shadow: 0 4px 16px rgba(20,24,40,.06);
  transform: translateY(0);
  transition: transform .35s cubic-bezier(.4, 0, .2, 1);
}
.hos-head.is-hidden { transform: translateY(-100%); }

.hos-logo { font-size: 18px; font-weight: 800; color: #4f46e5; text-decoration: none; letter-spacing: .02em; }
.hos-nav { display: flex; gap: 4px; margin-left: auto; }
.hos-nav a {
  color: #353a45; text-decoration: none;
  font-size: 13px; font-weight: 600;
  padding: 7px 12px; border-radius: 8px;
  transition: background .2s ease, color .2s ease;
}
.hos-nav a:hover { background: #eef0fb; color: #4f46e5; }

.hos-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; padding-top: 56px; }
.hos-article { max-width: 600px; margin: 0 auto; padding: 28px 26px 90px; color: #2a2e38; line-height: 1.85; }
.hos-article h1 { font-size: 26px; font-weight: 800; margin: 6px 0 16px; letter-spacing: -.01em; }
.hos-lead {
  font-size: 15px; color: #515767;
  padding: 14px 18px; margin: 0 0 22px;
  border-left: 4px solid #6366f1;
  background: rgba(99,102,241,.06);
  border-radius: 0 10px 10px 0;
}
.hos-article p { margin: 0 0 18px; }

@media (prefers-reduced-motion: reduce) { .hos-head { transition: none; } }
JavaScript
// スクロール方向の差分でヘッダーを隠す/見せる
(() => {
  const sc = document.getElementById('hosScroll');
  const head = document.getElementById('hosHead');
  if (!sc || !head) return;

  let last = 0;
  const DELTA = 4;     // 小刻みな揺れを無視する閾値
  const TOP_LOCK = 60; // 最上部付近では常に表示

  function apply() {
    const y = sc.scrollTop;
    if (y < TOP_LOCK) {
      head.classList.remove('is-hidden');
    } else if (y - last > DELTA) {
      head.classList.add('is-hidden');     // 下方向 → 隠す
    } else if (last - y > DELTA) {
      head.classList.remove('is-hidden');  // 上方向 → 見せる
    }
    last = y;
  }

  let ticking = false;
  sc.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { apply(); ticking = false; });
  }, { passive: true });

  // 一度だけ下→上へ自動スクロールし、隠れる→現れるを実演
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce, dir = 1;
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    sc.addEventListener(ev, () => { auto = false; }, { passive: true }));

  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = sc.scrollHeight - sc.clientHeight;
      sc.scrollTop += dir * 2.2;
      if (sc.scrollTop >= Math.min(max, 240)) dir = -1;
      else if (sc.scrollTop <= 0) dir = 1;
      requestAnimationFrame(step);
    }, 1000);
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「下スクロールで隠れるヘッダー」の効果を追加してください。

# 追加してほしい効果
下スクロールで隠れるヘッダー(ヘッダー)
下方向スクロールでヘッダーを隠し、上方向に戻すと即座に再表示。読書領域を最大化しつつ操作性を保つ手法で、記事・ドキュメント・モバイルサイトに向きます。スクロール方向の差分で translateY を制御します。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 下スクロールで隠れ、上スクロールで戻るヘッダー -->
<div class="hos-frame">
  <header class="hos-head" id="hosHead">
    <a class="hos-logo" href="#" onclick="return false">◢ Lumen</a>
    <nav class="hos-nav">
      <a href="#" onclick="return false">ホーム</a>
      <a href="#" onclick="return false">特集</a>
      <a href="#" onclick="return false">連載</a>
      <a href="#" onclick="return false">購読</a>
    </nav>
  </header>

  <div class="hos-scroll" id="hosScroll">
    <article class="hos-article">
      <h1>読むときは消え、戻すと現れる</h1>
      <p class="hos-lead">下方向のスクロールではヘッダーを隠して本文に集中させ、上方向に少しでも戻すと即座に再表示します。スクロール方向の差分を見て translateY を切り替えるだけの軽量実装です。</p>
      <p>ニュース記事やドキュメント、モバイルの縦長ページで特に有効。常時固定だと狭い画面で本文を圧迫しますが、この方式なら必要な瞬間だけナビが戻ってきます。</p>
      <p>判定はシンプル。直前の scrollTop と比較して、増えていれば隠し、減っていれば見せる。最上部付近では必ず表示してチラつきを防ぎます。</p>
      <p>閾値とアニメーション速度はCSSとJSの定数だけで調整可能。プロジェクトのトーンに合わせて軽快にも上品にもできます。</p>
      <p>このデモは一度だけ自動で下→上へスクロールし、隠れる/現れるの両方を見せます。ホイールやタッチで触れると自動は停止します。</p>
      <p>最後の段落。スクロール方向ベースのヘッダー制御は、固定ヘッダーの「邪魔さ」を消しつつ到達性を残す折衷案として定番です。</p>
    </article>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.hos-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #f6f7fb; }

.hos-head {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 10;
  display: flex; align-items: center; gap: 18px;
  height: 56px; padding: 0 22px;
  background: #ffffff;
  border-bottom: 1px solid #eceef3;
  box-shadow: 0 4px 16px rgba(20,24,40,.06);
  transform: translateY(0);
  transition: transform .35s cubic-bezier(.4, 0, .2, 1);
}
.hos-head.is-hidden { transform: translateY(-100%); }

.hos-logo { font-size: 18px; font-weight: 800; color: #4f46e5; text-decoration: none; letter-spacing: .02em; }
.hos-nav { display: flex; gap: 4px; margin-left: auto; }
.hos-nav a {
  color: #353a45; text-decoration: none;
  font-size: 13px; font-weight: 600;
  padding: 7px 12px; border-radius: 8px;
  transition: background .2s ease, color .2s ease;
}
.hos-nav a:hover { background: #eef0fb; color: #4f46e5; }

.hos-scroll { height: 100%; overflow-y: auto; scrollbar-width: thin; padding-top: 56px; }
.hos-article { max-width: 600px; margin: 0 auto; padding: 28px 26px 90px; color: #2a2e38; line-height: 1.85; }
.hos-article h1 { font-size: 26px; font-weight: 800; margin: 6px 0 16px; letter-spacing: -.01em; }
.hos-lead {
  font-size: 15px; color: #515767;
  padding: 14px 18px; margin: 0 0 22px;
  border-left: 4px solid #6366f1;
  background: rgba(99,102,241,.06);
  border-radius: 0 10px 10px 0;
}
.hos-article p { margin: 0 0 18px; }

@media (prefers-reduced-motion: reduce) { .hos-head { transition: none; } }

【JavaScript】
// スクロール方向の差分でヘッダーを隠す/見せる
(() => {
  const sc = document.getElementById('hosScroll');
  const head = document.getElementById('hosHead');
  if (!sc || !head) return;

  let last = 0;
  const DELTA = 4;     // 小刻みな揺れを無視する閾値
  const TOP_LOCK = 60; // 最上部付近では常に表示

  function apply() {
    const y = sc.scrollTop;
    if (y < TOP_LOCK) {
      head.classList.remove('is-hidden');
    } else if (y - last > DELTA) {
      head.classList.add('is-hidden');     // 下方向 → 隠す
    } else if (last - y > DELTA) {
      head.classList.remove('is-hidden');  // 上方向 → 見せる
    }
    last = y;
  }

  let ticking = false;
  sc.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { apply(); ticking = false; });
  }, { passive: true });

  // 一度だけ下→上へ自動スクロールし、隠れる→現れるを実演
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce, dir = 1;
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    sc.addEventListener(ev, () => { auto = false; }, { passive: true }));

  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = sc.scrollHeight - sc.clientHeight;
      sc.scrollTop += dir * 2.2;
      if (sc.scrollTop >= Math.min(max, 240)) dir = -1;
      else if (sc.scrollTop <= 0) dir = 1;
      requestAnimationFrame(step);
    }, 1000);
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。