PPP's Blog

雑コード帳

jQueryでスティッキーヘッダーをつくる

スクロールによってグローバルナビゲーション等が隠れるのを避けるために、ヘッダー要素を固定する「スティッキーヘッダー」をjQueryで実装します。

実際に完成したもの

コード等含め、下記サイトで確認できます。 https://codepen.io/itsumoonazicode/full/abwqNdB

実装したコードの流れ

HTML, CSSは先述のサイトからご確認ください。JavaScriptのコードに絞って詳述します。

変数を定義する

必要な要素を変数として定義します。いつも忘れるのですが、offset()メソッドはドキュメントを起点として、指定した要素の位置を取得します。
参考:jQueryのoffset() で表示位置の取得と要素の移動

var $window = $(window),
    $main = $('main'),
    $header = $('.page-header'),
    headerOffsetTop = $header.offset().top,
    headerHeight = $header.outerHeight();

イベントを定義する

$window.on('load scroll', _.debounce(function() {
    // 処理を書く
}, 50));

ヘッダーを上部に固定させたいのは、以下のタイミングです。

  • 画面をスクロールしたことでヘッダーが隠れたタイミング

これを実現するために、スクロールイベントを定義します。合わせて、画面中腹でページがリロードされた場合などを考慮して、ロードイベントを定義します。(たとえば、アンカーリンクを経由してページを訪れると、いきなりページ中盤から表示されたりしますね。そういう場合でもヘッダーを上部に固定するためにロードもイベント対象に入れます。)

処理を間引く(debounce)

スクロールをイベント対象に入れて、その中でconsole.log($window.scrollTop())と書いてみると、少しのスクロールだけでコンソールがログで埋め尽くされるのが分かるかと思います。イベント内に書かれた処理の内容によっては、ブラウザの処理が重くなる場合が出てきます。それを防ぐために、イベントの実行頻度を下げる処理を入れています。
参考:lodash の debounce や throttle で簡単に負荷対策 - Qiita

$window.on('load scroll', _.debounce(function() {
    // 処理を書く
}, 50));

スクロール量とヘッダーの初期位置を比較する

if($window.scrollTop() > headerOffsetTop) {
    // 処理を書く
}

ヘッダーが隠れるタイミングで、ヘッダーを上部固定にするためにスクロール量とヘッダーの初期位置を比較します。こちらの記事「jQueryのoffset() で表示位置の取得と要素の移動」内【offset() の起点】を合わせて見てもらうとわかりやすいのですが、要素の(今回はヘッダー)offset().topをスクロール量が上回ると、要素の上部分が画面から隠れることになります。

ヘッダーを上部に固定したいのはまさにそのタイミングです。なので、条件分岐として『スクロール量 > ヘッダーの初期位置』を最初に書きます。

スクロール量が上回る場合、ヘッダーを上部に固定する

CSSはこちらのページからご確認ください。
https://codepen.io/itsumoonazicode/full/abwqNdB

if($window.scrollTop() > headerOffsetTop) {
  $header.addClass('sticky');
  $main.css('margin-top', headerHeight);
}

スクロール量がヘッダーの初期位置を上回ったタイミングで、固定用のクラスを付与します。

$main.css('margin-top', headerHeight);としているのは、ヘッダーを固定するために使うCSS position: fixed;が関連しています。これを指定された要素は、レイアウトから外されてしまいます。そのため、ヘッダーが上部に固定された瞬間に、元々ヘッダーの下にあったコンテンツが一気に上へせり上がることになり、コンテンツがヘッダーに隠れてしまいます。それを防ぐために、ヘッダーを上部に固定するタイミングで、ヘッダーの高さ分の余白をヘッダーの次に続く要素に与えています。

それ以外の場合

if($window.scrollTop() > headerOffsetTop) {
  // 当てはまるときの処理を書く
} else {
  $header.removeClass('sticky');
  $main.css('margin-top', '0');
}

スクロール量がヘッダーの初期位置を下回るタイミングで、上部固定用クラスを削除します。合わせて、ヘッダーの次に続く要素に付与した余白も0に変更します。

これで完成です。

おわりに

どうもdebounceの指定方法がおかしいのか、上手く上部固定が働きません…。実際にこのコードを使う場合には該当箇所からdebounceは外す必要がありますね。。ここを修正できたら再度記事を書こうと思います。