overflow-anchor を使って pin to bottom コンポーネントを実装する
はじめに
この記事は Implementing a pin-to-bottom scrolling element with only CSS を読んだ後、JavaScript を使った実装方法も交えてまとめたものだ。
最近のウェブサイトでは、新しいコンテンツが追加されるたびにスクロール位置を一番下へ調整するUIをよく見かける。例えば Twitch のチャット欄や:
YouTube のコメント欄などだ:
このような挙動を指す決まった用語はあまり見当たらないので、ここでは一旦「pin to bottom」と呼ぶことにする。
[JavaScript] どう実装するか?
function append(comment) {
const comments = document.querySelector(".comments")
comments.appendChild(comment)
comments.scrollTop = comments.scrollHeight
}
まずコンテナに固定の高さを設定し、overflow-y: auto を指定する。こうすることで、コンテンツがコンテナの高さを超えた場合にスクロールできるようになる。
あとは新しい要素を追加するたびに毎回 comments.scrollTop = comments.scrollHeight を呼び出し、scrollTop が常に scrollHeight の高さと等しくなるようにすればよい。
考慮すべき問題
この実装は非常にシンプルだが、1つ問題が生じる。例えば、ユーザーが過去のコンテンツを見直すために上へスクロールしている最中に新しいコメントが追加されると、強制的に一番下までスクロールされてしまう点だ。
これを解決するために、コンテナの現在の scrollTop を事前にチェックし、scrollHeight と一定の差がある場合は自動スクロールを行わないようにする。
if (
comments.offsetHeight < comments.scrollHeight &&
comments.scrollTop + comments.offsetHeight + 150 > comments.scrollHeight
) {
comments.scrollTop = comments.scrollHeight - comments.offsetHeight
}
ここでの offsetHeight はコンテナの高さ(CSS height)を表し、scrollHeight はコンテナ全体のスクロール可能な総高さを表す。そして scrollTop は現在のスクロール位置だ。
- まだ
offsetHeightを超えていなければ、下へスクロールする処理を定義する必要はない。 - ユーザーが
150px以上スクロールして離れている場合は、自動でスクロールさせない。
なお、自動スクロールを行わない場合、通常の実装ではボタンやインジケーターを追加して、ユーザーが最新のコメントへジャンプできるようにすることが多い。
さらに、コメントリストが非常に長くなるとパフォーマンス上の問題が発生する可能性がある。対処法としては以下のいくつかのアプローチがある:
- 最新の500件程度だけを残す(件数は実際の要件に応じて調整)
- windowing のような手法を使って最適化する。(ただし高さが不定の場合に対応できるか僕には確証がない)
overflow-anchor
今回紹介するのは、overflow-anchor を使って同様の効果を得る方法だ。このプロパティは Implementing a pin-to-bottom scrolling element with only CSS という記事で見つけたもので、記事内では overflow-anchor を利用して上記のような効果を実現している。
このプロパティは CSS Scroll Anchoring Module で定義されている。ユーザーが効率的にコンテンツを**閲覧(消費)**できるように、この仕様ドラフトではリサイズやスクロールが発生した際にアンカーとなるべきコンテンツを自動検出する overflow-anchor の仕組みが策定されている。
詳細なアルゴリズムの定義はこちらにある。
- スクロールコンテンツ S がスクロールされるたびに、属性が
noneでない子要素の中からアンカーノードを1つ選択する。 - ノード N がスクロールコンテンツ S 内にあり、かつ不可視である場合(原文では excluded subtree および fully clipped と記載、要するにスクロールボックス内で見えない状態)、何もしない。
- ノード N がスクロールコンテンツ S 内で可視である場合、N をアンカーとして選択する。
- ノード N がスクロールコンテンツ S 内で一部のみ可視である場合、子要素に対して上記アルゴリズムを引き続き適用してアンカーを探す。
実際にはドラフト内でさらに多くの挙動が定義されているので、興味があれば読んでみるといいだろう。ただ、大まかに何ができるかを把握しておくだけで十分だ。
まず、コンテナ内にアンカーを1つ追加する:
<ul class="comments">
<li class="anchor"></li>
</ul>
ここでの anchor には CSS プロパティ overflow-anchor: auto を設定し、ブラウザにアンカーとして認識させる。
.comments {
& > .comment {
overflow-anchor: none; // アンカーにしない
}
.anchor {
overflow-anchor: auto; // アンカーにする
}
}
そして、コメントを追加する処理を insertBefore に変更し、必ず anchor の手前に挿入されるようにする。
実測結果
左側は一般的なやり方で実装したもので、右側は overflow-anchor を使って実装したものだ。ただ、僕が試したところでは、一度スクロールさせないとアンカーの挙動が正しく動作しないようだった。
余談だが、デモ内のテキスト内容は僕が以前伊坂幸太郎の『ガソリン生活』を読んだ後に書いた記事のもので、手軽に用意するためにそこから何段落か適当にコピー&ペーストした。
現在のブラウザ対応状況は Firefox 66 および Chrome 56 であり、まだ広く普及しているとは言えないが、将来同様の機能を実装する際の新たなアプローチになるかもしれない。余分なアンカー要素を1つ追加しなければならないという欠点はあるものの、純粋な CSS だけで実現できるのはなかなか面白い手法だと思う。
関連記事
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。
- なぜウェブで Pixel Perfect を追求すべきではないのか Pixel Perfect はそれが本当に重要な場合にのみ意識すべきであり、そうでなければ往々にしてお互い損をする結果に陥ってしまう。