· 5分で読了

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 は現在のスクロール位置だ。

  1. まだ offsetHeight を超えていなければ、下へスクロールする処理を定義する必要はない。
  2. ユーザーが 150px 以上スクロールして離れている場合は、自動でスクロールさせない。

なお、自動スクロールを行わない場合、通常の実装ではボタンやインジケーターを追加して、ユーザーが最新のコメントへジャンプできるようにすることが多い。

さらに、コメントリストが非常に長くなるとパフォーマンス上の問題が発生する可能性がある。対処法としては以下のいくつかのアプローチがある:

  1. 最新の500件程度だけを残す(件数は実際の要件に応じて調整)
  2. windowing のような手法を使って最適化する。(ただし高さが不定の場合に対応できるか僕には確証がない)

overflow-anchor

今回紹介するのは、overflow-anchor を使って同様の効果を得る方法だ。このプロパティは Implementing a pin-to-bottom scrolling element with only CSS という記事で見つけたもので、記事内では overflow-anchor を利用して上記のような効果を実現している。

このプロパティは CSS Scroll Anchoring Module で定義されている。ユーザーが効率的にコンテンツを**閲覧(消費)**できるように、この仕様ドラフトではリサイズやスクロールが発生した際にアンカーとなるべきコンテンツを自動検出する overflow-anchor の仕組みが策定されている。

詳細なアルゴリズムの定義はこちらにある。

  1. スクロールコンテンツ S がスクロールされるたびに、属性が none でない子要素の中からアンカーノードを1つ選択する。
  2. ノード N がスクロールコンテンツ S 内にあり、かつ不可視である場合(原文では excluded subtree および fully clipped と記載、要するにスクロールボックス内で見えない状態)、何もしない。
  3. ノード N がスクロールコンテンツ S 内で可視である場合、N をアンカーとして選択する。
  4. ノード 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 だけで実現できるのはなかなか面白い手法だと思う。

関連記事

他のトピックを探索