2021.06.18

コーディング

【jQueryスムーススクロール】position:fixed、画像遅延ロード、コンテンツ表示の遅れ等によるズレの解消

スムーススクロールを扱う際、コンテンツ表示の順番や速度の遅れによって移動先がズレてしまう問題を解消してみました。
現状はうまくいっていますが、今後問題が判明すれば検討しなおします。

『position: fixed』によるズレ

そもそもスムーススクロールがズレる原因は2点あると思います。
一つ目は、『スムーススクロール ズレる』などで検索してみると散見される、『position: fixed』が指定されたヘッダー等の裏にコンテンツの上部が隠れてしまうズレです。

ホントはこれ隠れてしまっているだけでズレてる訳ではないんですが、ズレて見えるということですね。

こちらの解決法は、
『スムーススクロール』で検索すると、よく出てくるjQueryのコード👇

$(function(){
  $('a[href^="#"]').on('click', function(){
    var speed = 400;
    var href= $(this).attr("href");
    var target = $(href == "#" || href == "" ? 'html' : href);
    var position = target.offset().top;
    $("body,html").animate({scrollTop:position}, speed, "swing");
    return false;
  });
});

このコードにヘッダーの高さ分のバッファーを持たせる変数を追加し、移動先のスクロール位置から引き算すれば良いです。

$(function(){
  $('a[href^=#]').on('click', function(){
    var buffer = 100; //ヘッダーの高さ等
    var speed = 400;
    var href= $(this).attr("href");
    var target = $(href == "#" || href == "" ? 'html' : href);
    var position = target.offset().top - buffer;
    $('body,html').animate({scrollTop:position}, speed, 'swing');
    return false;
  });
});

レスポンシブでヘッダーの高さが変わる際はこのbufferの値を変更すれば対応できます。

また、上記の方法を使わずとも、スムーススクロール先が<h2>タグで固定だったりするならば、
cssで

h2 {
  padding-top: 100px;
  margin-top: -100px;
}

のようにネガティブマージンを使うことでh2要素自体の上部空間を広げて、ヘッダーの裏に隠れる部分を隠れても問題のない空白にしてしまうという方法もあります。
ただこちらの方法はpadding-top、margin-topを本来の意味で使いづらくなるので個人的には微妙かなと思います。

コンテンツ表示の遅れによるズレ

二つ目は、
・画像の遅延ローディング
・スクロールがトリガーになって表示されるコンテンツの存在
等によって目指したスクロール位置自体が移動途中で変わってしまうことによるズレです。

画像の遅延ローディングに関しては画像タグ自体にheightをあらかじめ指定しておけばレンダリング時点で高さが確保されるためズレない。さらに表示速度向上も見込めて一石二鳥! という話も聞きますが、
レスポンシブで画像の高さが変わればその分ズレてしまいますし、画像に合わせて高さの指定をいちいちするというのは個人的にあまりスマートではない気がします。

スクロールがトリガーになって表示されるコンテンツに関しても同様です。
高さをあらかじめ指定しておくというのは今後コンテンツの中身を変更する際に変更箇所が増えるし、次の人が見逃してしまう可能性なんかもあるので気が進みません。

コンテンツの変更も気にせずでき、jQueryのみで完結できる方法を試した結果、以下のようなコードにしてみたところ(現状では)うまくいったようなので紹介します。

var $body = $('body,html');


$('a[href^="#"]').on('click', function(e){
  var $header = $('.header');
  var buffer = 0;

  if ($header.length){
  buffer += $header.innerHeight();
  }

  var speed = 400;
  var href = $(this).attr("href");
  var target = $(href == "#" || href == "" ? 'html' : href);
  var position = Math.floor(target.offset().top - buffer);

  smoothScroll(target, position, speed, buffer, 0);
  return false;
});

function smoothScroll(target, position, speed, buffer, count){
  count++;
  if (count > 9 ) return false;

  $body.animate({ scrollTop: position }, speed, 'swing', function(){
    var dest = Math.floor(target.offset().top - buffer);
    if(dest == position){
      return true;
    } else {
      scrollsmooth(target, dest, 0, buffer, count);
    }
  });
}

jQueryのanimate関数は第四引数にアニメーション完了後に実行する関数を設定できます。

アニメーション完了後に再度目的のスクロール位置を取得し、元々目指していた位置と一致すればズレなしで完了。
一致しなければ目指すスクロール位置を更新し、自身を再帰呼び出しするという流れです。

『if (count > 9 ) return false;』の部分は予想外の無限ループが起きるかもしれないと思い保険でいれておきました。
基本的に再帰されるのは一回だけで無限スクロールに陥ったことは今のところないのでcountの引数自体とっぱらっちゃってもいいかもしれませんが、再帰処理では慎重になっておいて損はないかなぁと……

以上、なにかの参考になれば幸いです。

ご覧いただきありがとうございました。
今後不具合が起きた場合は追記いたします。

この記事を書いた人

葉っぱ一号

葉っぱ一号

フロントエンドエンジニア

おいしいお店を探すのが好きです。おいしいお店のために遠出もしちゃいます。遠出したい衝動のためにおいしいお店を探すのかもしれません。そんなものですよね。