LPなどの長いページを作成した場合に、上の方にあるボタンなどのページ内リンクをクリックしたときに、アンカーとなっているidを設置した要素にたどり着かずに、その上で途中で止まってしまうことがあります。
この挙動の対処法をまとめています。
なぜ内部リンクによるスクロールが途中で止まるのか?
長いLPの場合、LazyLoadによって下の方の要素がすぐには読み込まれないことがあります。
このため、要素が読み込まれていない状態で内部リンクのボタンなどをクリックすると、その要素の位置まで飛びますが、とんだ後に周囲の要素が読み込まれ、対象の要素が下へ下へと下がってしまい、最終的な到着地は指定した要素よりも上の位置になってしまいます。
まとめると以下の3つが要因となります。
・要素の未読み込みと初期スクロール
ユーザーが内部リンクをクリックした際、ブラウザはそのリンクが指すIDを持つ要素の現在の位置を計算し、そこまでスクロールしようとします。しかし、LazyLoadが適用されている場合、まだ画面に表示されていない(またはスクロール範囲に近づいていない)下方の画像や動画、その他のセクションはまだ読み込まれていません。そのため、ブラウザは「要素がない」と判断し、本来の高さよりも短いページとして位置を計算します。
・遅延読み込みによるレイアウトシフト
スクロールが完了したとブラウザが判断した後、またはスクロール中に、遅れて(LazyLoadによって)下方の画像や広告、JavaScriptによって動的に生成されるコンテンツなどが読み込まれます。これらの要素が読み込まれると、それまで「存在しない」とされていたスペースが埋まり、ページ全体の高さがぐっと伸びます。
・対象要素の「ずれ」
ページ全体の高さが伸びると、すでにスクロールして到達したはずの目的の要素は、その下に新しいコンテンツが挿入されたことで、相対的に下に押しやられてしまいます。結果として、ユーザーの視点から見ると、内部リンクでジャンプしたはずの場所よりも上の位置でスクロールが止まってしまったように見えるのです。
対処法
対処法はスクロールした後に、自分がいる位置と、対象の要素の位置の差を比較し、差が生じている場合はスクロールを繰り返します。
これにより、対象の要素の本来の位置にたどり着くまでスクロールを繰り返します。
JavaScriptに以下のコードを追記します。
//申し込みフォームまで段々スクロールする
document.addEventListener('click', e => {
const link = e.target.closest('a[href="#CvForm"]');
if (!link) return;
e.preventDefault();
const targetId = link.getAttribute('href').slice(1);
const targetElement = document.getElementById(targetId);
if (!targetElement) return;
const header = document.querySelector('.elementor-header');
const headerHeight = header ? header.offsetHeight : 80;
setTimeout(() => {
smoothScrollToElement(targetElement, headerHeight);
}, 200);
});
function smoothScrollToElement(targetElement, headerHeight = 80) {
let previousScrollY = -1;
const checkAndScroll = () => {
if (!targetElement) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
if (Math.abs(scrollY - previousScrollY) < 2) {
// スクロールほぼ停止
return;
}
previousScrollY = scrollY;
const elementY = targetElement.getBoundingClientRect().top + scrollY;
const targetY = elementY - headerHeight;
window.scrollTo({ top: targetY, behavior: 'smooth' });
setTimeout(checkAndScroll, 100);
};
checkAndScroll();
}
大きな流れとしては、addEventListenerでクリックイベントを検知し、指定した要素がクリックされたなら対象の要素までスクロールする処理を繰り返します。
コードの解説
document.addEventListenerによるクリックの検地
document.addEventListener('click', e => {
ページ全体(document)に対して、クリックイベントを登録します。これにより、どこをクリックしても e(イベントオブジェクト)を受け取ることができます。
const link = e.target.closest('a[href="#CvForm"]');
if (!link) return;
クリックされた要素(e.target)が、 href=”#CvForm”と言う属性をもったaタグであるかを確認します。
closest()メソッドは、引数にCSSセレクタを指定します。上方向(親、親の親、…と遡っていく)にDOMツリーを探索し、引数で指定されたCSSセレクタに最初に合致する祖先要素を返します。
合致する祖先要素が見つからない場合は null を返します。
これをlinkという変数に代入し、該当しない場合(他のリンクや要素をクリックした場合)は何もしないで終了します。
e.preventDefault();
独自のスクロール処理を行うため、通常のリンク動作(スクロール)を無効化します。
const targetId = link.getAttribute('href').slice(1);
const targetElement = document.getElementById(targetId);
if (!targetElement) return;
これは、スクロール先の要素を取得する処理です。
href="#CvForm"
の #
を取り除いて "CvForm"
とし、getElementById
でid="CvForm"
の要素を取得します。該当する要素がなければ終了します。
const header = document.querySelector('.elementor-header');
const headerHeight = header ? header.offsetHeight : 80;
固定ヘッダーがあると、リンククリックでスクロールしたあとに対象の要素がヘッダーに覆われて隠れてしまうことがあります。
これを防ぐために、ヘッダーの高さを取得します。
クラスelementor-headerでElementor で作られたヘッダーの高さを取得します。存在しない場合は、デフォルトで 80px としています。
setTimeout(() => {
smoothScrollToElement(targetElement, headerHeight);
}, 200);
ページの遷移直後やアニメーション処理と被らないようにするため、200ミリ秒遅らせてスクロール処理を実行します。
smoothScrollToElement関数の内容
指定した要素のクリックを検知したらsmoothScrollToElement関数を実行します。
function smoothScrollToElement(targetElement, headerHeight = 80) {
引数として、ジャンプ先の要素「targetElement」とヘッダーの高さを渡します。
let previousScrollY = -1;
前回スクロールした際の位置を記録するために変数「previousScrollY」を定義します。デフォルト値は-1にしておきます。
checkAndScrollでスクロールを繰り返す
const checkAndScroll = () => {
前回のスクロール位置と目標の要素の位置を比較して、再帰的にスクロールを繰り返すための関数checkAndScrollの処理です。
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
if (Math.abs(scrollY - previousScrollY) < 2) {
return; // スクロールが止まったら終了
}
現在の位置を取得して、前回の位置と比較し2px以下なら処理を停止。それ以外は、次の処理へと移ります。
indow.pageYOffsetはブラウザがスクロールされているときに、縦にどれだけスクロールされたか(ピクセル単位)を取得するメソッドです。
たとえば、ページの一番上にいるときは 0、500px 下にスクロールすると 500 になります。
古いブラウザではindow.pageYOffsetが使えない場合があります。そのときのために、document.documentElement.scrollTopも設定しておきます。
これは、主に古いブラウザ(特に IE)で使われていた、縦方向のスクロール位置を取得するプロパティです。(なお、一部のブラウザでは document.body.scrollTop を使う場合もありましたが、これは現代ではあまり使われません)
互換性のためのフォールバック(保険的な書き方)です。
const elementY = targetElement.getBoundingClientRect().top + scrollY;
絶対的な上部位置からどれだけ離れているかを計算します。
.getBoundingClientRect()
DOM要素が持つメソッドで、呼び出された要素のサイズと、ビューポート(ブラウザの表示領域)に対する位置を示すDOMRectオブジェクトを返します。
このオブジェクトには、top, right, bottom, left, width, height といったプロパティが含まれます。
.top
getBoundingClientRect()が返すDOMRectオブジェクトのtopプロパティです。
要素の上端がビューポートの最上部からどれだけ離れているかをピクセル単位で示します。
(注意点: この値は、スクロールによって変化します。要素がビューポートの上部に行けば行くほど、topの値は小さくなり、ビューポートの最上部にある場合は0、ビューポートより上にある場合は負の値になります)
つまり、targetElement.getBoundingClientRect().top はビューポートから見た要素の上端位置です。
targetElement.getBoundingClientRect().top は、スクロールによって値が変わります。(スクロールすると、topの値は小さくなる。)
これに、「scrollY」windowオブジェクトのプロパティで、現在のドキュメントが垂直方向(Y軸)にどれだけスクロールされているかを示すピクセル値です。
scrollY は、スクロール量そのものです。(スクロールすると、scrollYの値は大きくなる。)
この2つを足し合わせることで、ユーザーがページをスクロールした場合でも、targetElementがページの絶対的な上端(一番上の0地点)から何ピクセル下にあるかという、固定された位置を計算することができます。
const targetY = elementY - headerHeight;
追加のスクロール先の位置は、要素の絶対的な位置からヘッダーの位置を引いたものにします。これでヘッダーの被りを防ぐことができます。
window.scrollTo({ top: targetY, behavior: 'smooth' });
スクロール先を指定して、スムーズにスクロールします。
setTimeout(checkAndScroll, 100);
100ms後に再度、checkAndScrollを実行します。
指定した要素と実際の位置が2px以下になるまでこの処理を繰り返します。
以上で処理は完了です。