ページを下にスクロールするとヘッダーが隠れ、上にスクロールすると再び現れる動きは、多くのWebサイトで採用されています。コンテンツの閲覧を邪魔せず、ナビゲーションが必要なときにすぐアクセスできるユーザーフレンドリーなUI パターンです。
本記事では、jQuery を使わない Vanilla JavaScript で実装する方法を、基本からパフォーマンス最適化、モバイル対応まで段階的に解説します。
この記事でわかること
・上向きスクロールで表示されるヘッダーの仕組み
・HTML + CSS + Vanilla JS の基本実装
・CSS transition でスムーズにスライドインさせる方法
・requestAnimationFrame / throttle によるパフォーマンス最適化
・ページトップでは常に表示するロジック
・モバイルの慣性スクロール(オーバースクロール)対策
・コピーしてすぐ使える完成コード
・上向きスクロールで表示されるヘッダーの仕組み
・HTML + CSS + Vanilla JS の基本実装
・CSS transition でスムーズにスライドインさせる方法
・requestAnimationFrame / throttle によるパフォーマンス最適化
・ページトップでは常に表示するロジック
・モバイルの慣性スクロール(オーバースクロール)対策
・コピーしてすぐ使える完成コード
仕組みの概要
| スクロール方向 | ヘッダーの動作 |
|---|---|
| 下にスクロール | ヘッダーが上に隠れる(translateY で画面外へ) |
| 上にスクロール | ヘッダーがスライドインして表示される |
| ページトップ付近 | 常に表示(隠す必要がない) |
実装のポイント
(1)
(2) 前回の位置と比較してスクロール方向(上 / 下)を判定
(3) 方向に応じてヘッダーに CSS クラスを付け外し
(4) CSS transition でアニメーションを制御
(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: none や visibility: 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 イベントが発火します。基本版は分かりやすさ重視ですが、実際のサイトでは次のセクションで紹介するrequestAnimationFrame や throttle でパフォーマンスを最適化してください。
スクロール中は 1 秒間に数十回 scroll イベントが発火します。基本版は分かりやすさ重視ですが、実際のサイトでは次のセクションで紹介するrequestAnimationFrame や throttle でパフォーマンスを最適化してください。
パフォーマンス最適化版
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スクロール時にヘッダーがチラつきます
A
SCROLL_THRESHOLD(閾値)を設定して微小なスクロールを無視してください。本記事の完成版コードでは 5px の閾値を設定しています。モバイルの慣性スクロールで特にチラつきが出やすいため、閾値は 3〜10px 程度に調整してください。Qページ内リンク(アンカーリンク)でヘッダーに被ります
ACSS で
scroll-padding-top: 60px(ヘッダーの高さ)を html 要素に設定すると、アンカーリンクの移動先がヘッダーに被らなくなります。詳細は「追従ヘッダーの高さに合わせてアンカーリンクを調整する方法」を参照。Q{ passive: true } は何ですか?
Ascroll イベントリスナーに
{ passive: true } を付けると、ブラウザに「この関数は preventDefault() を呼ばない」と伝えます。スクロールのジャンク(カクつき)を防止する効果があり、特にモバイルで効果的です。Qwill-change: transform は必要ですか?
A
will-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 追従ヘッダーの実装完全ガイド」も併せて参照してください。

