少し前の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つ。