CSS Transitionを使ったスムーズにスクロールしてトップに戻る機能

スムーズニトップヘモドル。

前に作ったスクロールした時に位置固定のロゴをトップに戻る機能にすり替えるものを少し手直しして再導入した。今回はスムーズにスクロールさせようかと色々考えていたが、やはりJavaScriptでscrollTo()を制御するのはコストが高い。CSSならどうだと試行錯誤したところ、どうやらbody要素への負のマージンをCSS Transitionで滑らかに変化させれば良いようだ。

Demo: Scroll Smoothly with CSS Transition

デモのページにはダミーテキストの各セクションの最後にそれぞれ⇑ Back to Topというリンクがある。それをクリックすると1秒かけてスムーズにスクロールしながらトップに戻る。トリガーとスクロール自体はJavaScriptで行っているが、スクロールのアニメーション自体はCSS Transitionで行っている。具体的には以下のような処理と仕組みになる。

  1. トップに戻るリンクをクリック
  2. 現在のスクロール距離(window.pageYOffset)分だけマイナスにbody要素のmargin-topプロパティーを設定する
  3. スクロールしてトップへ戻す
  4. transitionプロパティーをbody要素のmargin-topプロパティーへ仕込む
  5. body要素のmargin-topプロパティーの値を0にする
  6. CSS Transitionでmargin-topプロパティーの値を戻すアニメーションが起こる

スクロールバー(があるなら)を注意深く見るとわかるが、実際にはスムーズにスクロールしているわけではなく、そのように見えるというだけである。CSS Transitionによるアニメーションはユーザーが途中で止める手段もないので、実用上はほとんど問題ないだろう。

実装のコードも9行ほどと短く、色々なものに組み込みやすいと思われる。コードも順序良くCSSを割り当てていくだけだ。

document.getElementById("to-top").addEventListener("click", function (evt) {
  var styleBody = document.body.style;
  styleBody.transition = "initial";
  styleBody.marginTop = "-" + (window.pageYOffset - 1) + "px";
  window.scrollTo(0, 0);
  styleBody.transition = "margin-top 1s ease-in-out";
  styleBody.marginTop = "0";
  evt.preventDefault();
});

最初にbody要素のtransitionプロパティーをinitialに戻すことで、負のマージンを与えた時にアニメーションしないようにすることができる。最初にこうしておかないと、複数回このトップに戻る機能を利用した場合におかしなことになってしまう。initialというキーワードの値はInternet Explorer 11を始めいくつかのブラウザーでまだサポートされていないが、不正な値を仕込んでも目的であるアニメーションを潰すことは可能だ。そのため行儀が良いとは言えないが、このままでも良いだろう。気になる人はその仕様で決まっている初期値であるall 0s ease 0sにすると良い。

この辺りのアニメーションに使ったスタイルの後始末は、transitionEndイベントできれいに行える。明示的にスタイルをきちんと元に戻すことができるので、バグが潜みづらい実装になるはずだ。複雑なCSSを持つウェブサイトではそうすることも考えるべきだ。

スクロール位置を取得するwindow.pageYOffsetから1を引いているのは、スクロールバーが一瞬消えてしまわないようにするためのおまじないだ。これは僕は気づいておらず、@ginpei_jpが考えてくれた。これがないとスクロールしきった状態でこのトップに戻る機能を利用すると、スクロールバーが一瞬消え、場合によってはレイアウトが一瞬ずれる可能性がある。

transitionプロパティーの長さとアニメーション関数は自分の好みでもっと色々試してみると良い。僕の感触では、長さが.5sでアニメーション関数はease-in-outとするのが一番スカッとスクロールしてくれると感じた。長さは移動距離に応じて調節しても良さそうだが、同じ時間で動かした方が好ましく感じる人が多いだろう。


このウェブサイトでは位置固定のロゴも同じようにスムーズにスクロールさせる必要があるので、ほんの少し実装がややこしくなっている。といってもロゴのmargin-topプロパティーでbody要素とは逆に正のマージンでずらしてやり、同じようにアニメーションさせているだけだ。うまく機能しているように思う。

ここまでくればPure CSSでもいけそうに思えるが、それはちょっと難しそうだ。アニメーションまではHTMLの助けを借りれば容易に実装することができるが、transitionプロパティーのリセットまたは:targetの解除がCSSだけではできない。

昨今は、とにかくCSSをうまく利用してアニメーションをさせた方が軽いものになることが多い。機能の発動のためのイベントなどでJavaScriptを使う事にはなるだろうが、それ以外では頭をひねってCSS Transition (またはAnimation)でやることを念頭に置くと、挙動と実装が共にスカッとしたアニメーションを提供できることだろう。