【JavaScript】上向きスクロールで表示されるヘッダーの作り方|Vanilla JS・CSS transition・throttle・モバイル対応まで解説

【JavaScript】上向きスクロールで表示されるヘッダーの作り方|Vanilla JS・CSS transition・throttle・モバイル対応まで解説 JavaScript

ページを下にスクロールするとヘッダーが隠れ、上にスクロールすると再び現れる動きは、多くのWebサイトで採用されています。コンテンツの閲覧を邪魔せず、ナビゲーションが必要なときにすぐアクセスできるユーザーフレンドリーなUI パターンです。

本記事では、jQuery を使わない Vanilla JavaScript で実装する方法を、基本からパフォーマンス最適化モバイル対応まで段階的に解説します。

この記事でわかること
・上向きスクロールで表示されるヘッダーの仕組み
・HTML + CSS + Vanilla JS の基本実装
・CSS transition でスムーズにスライドインさせる方法
・requestAnimationFrame / throttle によるパフォーマンス最適化
・ページトップでは常に表示するロジック
・モバイルの慣性スクロール(オーバースクロール)対策
・コピーしてすぐ使える完成コード
スポンサーリンク

仕組みの概要

スクロール方向 ヘッダーの動作
下にスクロール ヘッダーが上に隠れる(translateY で画面外へ)
上にスクロール ヘッダーがスライドインして表示される
ページトップ付近 常に表示(隠す必要がない)
実装のポイント
(1) scroll イベントで現在のスクロール位置を取得
(2) 前回の位置と比較してスクロール方向(上 / 下)を判定
(3) 方向に応じてヘッダーに CSS クラスを付け外し
(4) CSS transition でアニメーションを制御

HTML の準備

HTML
<header class="site-header">
  <nav>
    <a href="/" class="logo">MySite</a>
    <ul class="nav-links">
      <li><a href="/about">About</a></li>
      <li><a href="/blog">Blog</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
</header>
<main class="content">
  <!-- ページコンテンツ -->
</main>

CSS の実装

CSS
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 60px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
  /* スムーズなスライドアニメーション */
  transition: transform 0.3s ease;
}

/* 下スクロール時: ヘッダーを上に隠す */
.site-header.is-hidden {
  transform: translateY(-100%);
}

/* ヘッダーの高さ分だけコンテンツを下にずらす */
.content {
  padding-top: 60px;
}
transform: translateY(-100%) で隠す理由
display: nonevisibility: hidden ではアニメーションができません。transform: translateY(-100%) なら CSS transition でスムーズにスライドイン / アウトでき、GPU アクセラレーションも効くためパフォーマンスが良好です。

JavaScript の基本実装

JavaScript(基本版)
const header = document.querySelector(".site-header");
let lastScrollY = 0;

window.addEventListener("scroll", () => {
  const currentScrollY = window.scrollY;

  if (currentScrollY > lastScrollY && currentScrollY > 60) {
    // 下スクロール & ヘッダーの高さを超えた位置
    header.classList.add("is-hidden");
  } else {
    // 上スクロール or ページトップ付近
    header.classList.remove("is-hidden");
  }

  lastScrollY = currentScrollY;
});
変数 / 条件 役割
lastScrollY 前回の scroll イベント時のスクロール位置を保持
currentScrollY > lastScrollY 下にスクロールしている(位置が増加)
currentScrollY > 60 ページトップ付近(ヘッダーの高さ以内)では隠さない
scroll イベントは高頻度で発火する
スクロール中は 1 秒間に数十回 scroll イベントが発火します。基本版は分かりやすさ重視ですが、実際のサイトでは次のセクションで紹介するrequestAnimationFramethrottle でパフォーマンスを最適化してください。

パフォーマンス最適化版

requestAnimationFrame を使う方法

JavaScript(requestAnimationFrame版)
const header = document.querySelector(".site-header");
let lastScrollY = 0;
let ticking = false;

function updateHeader() {
  const currentScrollY = window.scrollY;

  if (currentScrollY > lastScrollY && currentScrollY > 60) {
    header.classList.add("is-hidden");
  } else {
    header.classList.remove("is-hidden");
  }

  lastScrollY = currentScrollY;
  ticking = false;
}

window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(updateHeader);
    ticking = true;
  }
});
requestAnimationFrame でフレームレートに同期
requestAnimationFrame はブラウザの描画タイミング(通常 60fps)に合わせて処理を実行します。scroll イベントが 1 フレーム内に複数回発火しても、実際の DOM 操作は 1 回だけになるためパフォーマンスが大幅に向上します。

throttle を使う方法

JavaScript(throttle版)
const header = document.querySelector(".site-header");
let lastScrollY = 0;
let throttleTimer = null;

function onScroll() {
  const currentScrollY = window.scrollY;

  if (currentScrollY > lastScrollY && currentScrollY > 60) {
    header.classList.add("is-hidden");
  } else {
    header.classList.remove("is-hidden");
  }

  lastScrollY = currentScrollY;
}

// 100ms ごとに最大 1 回実行
window.addEventListener("scroll", () => {
  if (throttleTimer) return;
  throttleTimer = setTimeout(() => {
    onScroll();
    throttleTimer = null;
  }, 100);
});
方式 メリット 実行頻度
イベント直接 シンプル 毎回(数十回/秒)
requestAnimationFrame フレームレートに同期。推奨 最大 60 回/秒
throttle(100ms) 実行間隔を制御 最大 10 回/秒

実用的な完成版コード

モバイル対応、ページトップでの挙動、スクロール量の閾値を組み込んだ実用版です。

JavaScript(完成版)
(() => {
  const header = document.querySelector(".site-header");
  if (!header) return;

  const HEADER_HEIGHT = header.offsetHeight;
  const SCROLL_THRESHOLD = 5; // 微小スクロールを無視
  let lastScrollY = 0;
  let ticking = false;

  function update() {
    const currentScrollY = window.scrollY;
    const diff = currentScrollY - lastScrollY;

    // ページトップ付近では常に表示
    if (currentScrollY <= HEADER_HEIGHT) {
      header.classList.remove("is-hidden");
      lastScrollY = currentScrollY;
      ticking = false;
      return;
    }

    // 微小スクロール(閾値以下)は無視(チラつき防止)
    if (Math.abs(diff) < SCROLL_THRESHOLD) {
      ticking = false;
      return;
    }

    if (diff > 0) {
      // 下スクロール → 隠す
      header.classList.add("is-hidden");
    } else {
      // 上スクロール → 表示
      header.classList.remove("is-hidden");
    }

    lastScrollY = currentScrollY;
    ticking = false;
  }

  window.addEventListener("scroll", () => {
    if (!ticking) {
      requestAnimationFrame(update);
      ticking = true;
    }
  }, { passive: true });
})();
改善ポイント 内容
SCROLL_THRESHOLD 5px 未満の微小スクロールを無視(モバイルのチラつき防止)
HEADER_HEIGHT チェック ページトップ付近(ヘッダー高さ以内)では常に表示
requestAnimationFrame フレームレートに同期して DOM 操作を最小化
{ passive: true } scroll イベントをパッシブにしてスクロールのジャンクを防止
即時実行関数 グローバルスコープを汚染しない

完成版の CSS(再掲 + 追加)

CSS(完成版)
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 60px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
  transition: transform 0.3s ease;
  will-change: transform;  /* GPU アクセラレーションを明示 */
}

.site-header.is-hidden {
  transform: translateY(-100%);
  box-shadow: none;  /* 隠れている間は影を消す */
}

.content {
  padding-top: 60px;  /* ヘッダーの高さ分 */
}

モバイル対応の注意点

問題 対策
慣性スクロール(バウンス)でチラつく SCROLL_THRESHOLD で微小変化を無視
アドレスバーの表示/非表示でスクロール位置が変化 window.scrollY の負値チェック(Math.max(0, …))
タッチ操作でスクロールが頻繁に発火 { passive: true } でスクロールパフォーマンスを確保
ヘッダーの高さがレスポンシブで変わる header.offsetHeight で動的に取得(固定値を避ける)
JavaScript(モバイル向け追加対策)
// iOS の慣性スクロールで scrollY が負になる場合の対策
const currentScrollY = Math.max(0, window.scrollY);

// 画面の向き変更時にヘッダーの高さを再取得
let headerHeight = header.offsetHeight;
window.addEventListener("resize", () => {
  headerHeight = header.offsetHeight;
});

CSS のみで実装する方法(position: sticky)

「上スクロール時だけ表示」はCSSだけでは実現できませんが、常に上部に固定するだけなら CSS の position: sticky で JavaScript 不要で実装できます。

CSS(sticky ヘッダー: JS 不要)
.site-header {
  position: sticky;
  top: 0;
  z-index: 1000;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* sticky はスクロールしても常に表示される */
/* 「上スクロール時だけ表示」は JS が必要 */

CSS の sticky ヘッダーの詳細は「CSS 追従ヘッダーの作り方」を参照してください。position: sticky が効かない場合は「position: sticky が効かない原因と解決方法」を参照してください。

バリエーション

一定量スクロールしてから隠し始める

JavaScript
// 200px 以上スクロールしてから隠し始める
const HIDE_AFTER = 200;

if (diff > 0 && currentScrollY > HIDE_AFTER) {
  header.classList.add("is-hidden");
} else if (diff < -SCROLL_THRESHOLD) {
  header.classList.remove("is-hidden");
}

ヘッダーの背景を透明→不透明に切り替え

JavaScript + CSS
/* CSS */
.site-header {
  background: transparent;
  transition: background 0.3s, transform 0.3s;
}
.site-header.is-scrolled {
  background: rgba(255, 255, 255, 0.95);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* JS(update 関数内に追加) */
if (currentScrollY > 10) {
  header.classList.add("is-scrolled");
} else {
  header.classList.remove("is-scrolled");
}

ヘッダーを縮小するパターン

CSS
.site-header {
  height: 80px;
  transition: height 0.3s, transform 0.3s;
}
.site-header.is-compact {
  height: 50px;  /* スクロール後は小さくなる */
}

アクセシビリティの考慮

考慮点 対策
キーボードナビゲーション Tab キーでヘッダー内のリンクにフォーカスが当たったときはヘッダーを表示する
prefers-reduced-motion アニメーションを無効にするメディアクエリに対応
スクリーンリーダー ヘッダーに aria-hidden を使わない(常に読み上げ可能にする)
CSS(アニメーション無効化の対応)
@media (prefers-reduced-motion: reduce) {
  .site-header {
    transition: none;
  }
}
JavaScript(フォーカス時にヘッダーを表示)
// Tab キーでヘッダー内にフォーカスが入ったら表示
header.addEventListener("focusin", () => {
  header.classList.remove("is-hidden");
});

よくある質問

QjQuery 版との違いは?
A本記事は Vanilla JavaScript(jQuery 不要)で実装しています。jQuery を使う方法は「jQuery 追従ヘッダーの実装完全ガイド」を参照してください。Vanilla JS ならファイルサイズが小さく、依存ライブラリなしで動作します。
Qスクロール時にヘッダーがチラつきます
ASCROLL_THRESHOLD(閾値)を設定して微小なスクロールを無視してください。本記事の完成版コードでは 5px の閾値を設定しています。モバイルの慣性スクロールで特にチラつきが出やすいため、閾値は 3〜10px 程度に調整してください。
Qページ内リンク(アンカーリンク)でヘッダーに被ります
ACSS で scroll-padding-top: 60px(ヘッダーの高さ)を html 要素に設定すると、アンカーリンクの移動先がヘッダーに被らなくなります。詳細は「追従ヘッダーの高さに合わせてアンカーリンクを調整する方法」を参照。
Q{ passive: true } は何ですか?
Ascroll イベントリスナーに { passive: true } を付けると、ブラウザに「この関数は preventDefault() を呼ばない」と伝えます。スクロールのジャンク(カクつき)を防止する効果があり、特にモバイルで効果的です。
Qwill-change: transform は必要ですか?
Awill-change: transform はブラウザに「この要素は transform が変化する」と事前に伝え、GPU アクセラレーションを準備させます。スクロール時のアニメーションがよりスムーズになりますが、多用するとメモリを消費するため、ヘッダーのように実際にアニメーションする要素だけに付けてください。
QReact / Vue でも同じロジックで実装できますか?
Aはい。スクロール方向の判定ロジックは同じです。React なら useEffect + useState、Vue なら onMounted + ref で scroll イベントリスナーを登録し、同じ判定ロジックで state / ref を更新してください。

まとめ

上向きスクロールで表示されるヘッダーの実装ポイントをまとめます。

ポイント 内容
スクロール方向の判定 window.scrollY の前回値と現在値を比較(増加=下、減少=上)
非表示の方法 transform: translateY(-100%) + CSS transition(GPU アクセラレーション)
パフォーマンス requestAnimationFrame で DOM 操作をフレームに同期
チラつき防止 SCROLL_THRESHOLD(5px 程度)で微小スクロールを無視
ページトップ対策 scrollY がヘッダー高さ以内なら常に表示
モバイル対応 { passive: true } + 負の scrollY チェック
アクセシビリティ prefers-reduced-motion + focusin でのヘッダー表示

CSS 固定ヘッダーの詳細は「CSS 追従ヘッダーの作り方」、jQuery 版は「jQuery 追従ヘッダーの実装完全ガイド」も併せて参照してください。