下方向へスムーズにスクロール。

少し前のCSS Transitionを使ったスムーズにスクロールしてトップに戻る機能という記事では、CSS Transitionを使ってスムーズにスクロールさせるためにbody要素のmargin-topプロパティーを負の値を設定した。これはとにかく上方向へのスクロールにしか使うことは出来ない。下方向にスムーズにスクロールさせるためにはまた別のアプローチが必要になるようだ。

何かしらのCSSプロパティーを使い、body要素を上方向にずらすということになる。つまりtransformプロパティーでtranslate()translate3d()を使いY方向のマイナスへ動かすのが向いているようだ。あとはtransitionプロパティーを組み合わせるだけでスムーズにスクロール(しているように見せる)ことができる。

Demo: Scroll Down Smoothly with CSS Transition

デモではJump to Bottomというボタンを押すとページの最下部までスクロールするようになっている。スムーズなことがわかりやすいように長めのアニメーションにしておいた。今回のものは任意の要素の位置へにもスクロールすることが可能だ。Jump to Middleというボタンでは中程までスクロールしてピタッと止まる。また上方向にも対応させた。Back to Topというボタンではページの先頭にスムーズにスクロールしながら戻ることができる。

function scrollToElm(to) {
  var root = document.documentElement;
  var styleRoot = root.style;
  var doScroll = function () {
    styleRoot.transition = styleRoot.transform = 'initial';
    window.scrollTo(0, to.offsetTop);
    root.removeEventListener('transitionend', doScroll, false);
  };
  var scrollDistance = window.pageYOffset - Math.min(
    to.offsetTop,
    root.scrollHeight - window.innerHeight
  );
  root.addEventListener('transitionend', doScroll, false);
  styleRoot.transition = 'transform 1s ease-in-out';
  styleRoot.transform = 'translate3d(0, ' + scrollDistance + 'px, 0)';
}

少しややこしくなったが、それでも16行程度で実装することができる。上下へスクロールさせる場合、後処理が重要になるのでtransitionendイベントを使うことが必須になる(使わなくても不可能ではない)。

まずは必要なスクロール量を考える。通常は現在のスクロール位置からスクロールして表示したい要素のoffsetTopを引いた量で良いが、ページの最後付近にある場合はページの最大スクロール量(ここではroot.scrollHeight - window.innerHeightで求めた)を引いた量に制限する必要がある。つまりこの2つのどちらか小さい方Math#min()を使って求め、現在のスクロール位置から引けば良い。

transformプロパティーではtranslate3d() (translateY()でも悪くはない)を使い、ドキュメント全体を動かす。そうするとスクロールしたように見える。もしスクロールバーがあるなら、それを注視すれば実際にはスクロールしていないことがわかるだろう。既にスクロール量の引き算で調節されているため、文字列連結でマイナスへ変換したりする必要はない。これにtransitionプロパティーで適当にアニメーションを追加すればスムーズになる。

実際のスクロールはアニメーションの終了後に行うことになる。CSS Transitionの終了時にはtransitionendというイベントが発火するので、これを使ってスクロール系のメソッドを使いスクロールさせる……前にtransformプロパティーをリセットしてやる。もちろんtransitionプロパティーも同時にリセットする必要がある。スクロールはElement.scrollIntoView()が一番直観的だが、要素のマージンなどにより微妙にずれることがあるため、window.scrollTo()を使う方が良いだろう。最後にこのもう必要のないイベント・ハンドラーは削除しておく。


これでCSS Transitionを利用したスムーズなスクロールを上下に行うことができるようになった。margin-topプロパティーを使ったものがハックに近い印象を受けるのと違って、このtransformプロパティーを使ったものは自然な実装に近いのもポイントが高い。

実際にはそれほどscrollTo()を使ったスムーズなスクロールは重くない。だがそれは普通の状態であるなら、だ。最近は非常に巨大な動画や埋め込みiframe要素、スクロールを阻害するもの(scrollイベントを使うもの)は多い。それらを考慮するとCSS Transitionのようなコストが低いかコストを他に(ここではCSS Transformに)丸投げできる技術を流用すると、安定した結果を得られるはずだ。

またスクロールのスムーズさをtransition-timing-functionプロパティーで容易にカスタマイズできる点も魅力的だ。ease-in-outでのなめらかな加速と減速はもちろん、linearをかわりに使えば平坦にもできる。ちょっと戻って勢いを付けてガッとスクロールさせる……といった更なるカスタマイズもcubic-bezier()を使えば不可能ではない(cubic-bezier(0.4, -0.4, 0.8, 1)とか)。steps()を使えばスキップしながらスクロールさせることすらできるだろう。現行の一部ブラウザーの開発者ツールには、そのアニメーションをGUIでプレビューしながら編集する機能もあり、その時には大いに助けになるはずだ。

このCSS TransitionとCSS Transformによるスムーズなスクロールの実装が万能なわけではない。ブラウザー側で全て制御させることになるので、場合によってはスムーズさとは無縁の結果になりうる。どう動くかはブラウザーの気分次第ということだ。その一方でアニメーションの制御コストと挙動カスタマイズという難題をブラウザーに丸投げできるという大きなメリットがある。ウェブページのコンテンツ次第では、スムーズにスクロールさせるための選択肢として最右翼になり得るはずだ。

追記

デモにいくつかあったバグを潰しておいた。うまく動かなかったと思った人は再度見てみてほしい。潰したバグは、Internet Explorer 11でCSSStyleDeclaration.transform = 'initial'が無視されるバグと、ベンダー拡張プリフィックスのつけ忘れにより(Mobile )Safari 8でアニメーションできないバグの2つ。