document.scrollingElementでスムーズスクロール

UI開発者 郡司

スムーズスクロールの実装はごく一般的なWebサイトであれば誰もが作る機能であり、実際に多くのWebサイトでその機能が実装されています。スムーズスクロールのよくある実装方法にjQueryを使用し、$('html, body')をスクロール対象としてアニメーションする方法をよく見ます。

html要素とbody要素を同時に指定しているのはレンダリングエンジンによって、スクロールする要素がhtml要素だったり、body要素だったりと実装にばらつきがあるためです。 この方法によってクロスブラウザでスムーズスクロールを実現することが可能となる一方で、ロジックによってはスムーズスクロール完了後のコールバックが2回呼ばれてしまうデメリットがあります。

例えばスムーズスクロール完了後に「移動後にURLを書き換える」「移動後に移動先の要素にフォーカスをあてる」などの機能を追加する場合は、コールバックが2回呼ばれないようにするための工夫が必要です。

しかし、今回紹介するdocument.scrollingElementを使用することで、コールバックを1回で済ませることができます。

document.scrollingElementとは

document.scrollingElementは、標準準拠モードではhtml要素を後方互換モードではbody要素を、スクロールできる要素として参照します。
MDNのドキュメントをみると、IE以外のほとんどのブラウザに実装されているのが確認できます。

document.scrollingElementで実装してみる

IE11は対応していないのでdocument.scrollingElementがない場合は、IE11でスクロールできる要素であるdocumentElementを返すように実装してみます。

実際のコードが次になります。

var scrollElem = (function(doc) {
    if ('scrollingElement' in doc) {
        return doc.scrollingElement;
    }

    return doc.documentElement;
})(document);

次のコードは、先ほどのコードを使用してスムーズスクロールでコールバックが1回だけ呼ばれるのを確認するためだけのコードです。

$('a[href*="#"]').on('click', function(event) {
    event.preventDefault();

    $(scrollElem).animate({
        scrollTop: $($(this).attr('href')).offset().top
    }, 800, function() {
        console.log('移動完了');
    });
});

jQueryのpromiseで実装してみる

コールバックが2回呼ばれないように工夫する方法として、jQueryのpromiseを利用する方法もあります。
jQueryを使用していると可読性という観点では代わり映えを感じませんが、jQueryを使用しないで書く場合はdocument.scrollingElementを使用したほうがシンプルかもしれません。

次のコードがjQueryのpromiseを使用したバージョンになります。

$('a[href*="#"]').on('click', function(event) {
    event.preventDefault();

    $('html, body').animate({
        scrollTop: $($(this).attr('href')).offset().top
    }, 800).promise().done(function() {
        console.log('移動完了');
    });
});

その他の実装方法

先ほど紹介した2つの方法以外にも、「ユーザーエージェントによるスクロール対象要素の分岐」「1回目のコールバック時にフラグを立て、2回目のコールバック時は処理を行わない」などの方法もありますので、実装するサイトの仕様などにあわせて実装方法を検討してみましょう。

まとめ

既存のスムーズスクロールを改修する際は、今回紹介した方法を実装してみることで思わぬバグの発生を防げるかもしれません。

また、スムーズスクロールのような頻繁に実装される機能はついコードの流用をしてしまいますが、この機会により良いコードを目指すために実装方法を見直してみはいかがでしょうか。