📜

要素追加時のコンテナ要素の高さやスクロールポート位置の急激な変化の取り扱いについて

に公開

個人的な思考の整理のために言語化した内容です。おそらく需要のない記事でしょうけれど、一応残しておきます。


スクロールコンテナー(スクロール可能な要素)において、現在表示されている部分領域はスクロールポートと呼ばれます。

https://developer.mozilla.org/ja/docs/Glossary/Scroll_container

╔════════════════╗ ┬
║ Lorem          ║ │ スクロールコンテナー
║ ipsum          ║ │ 
╟────────────────╢ │ ┬
║ dolor          ║ │ │ スクロールポート
║ sit            ║ │ │
╟────────────────╢ │ ┴
║ amet           ║ │ 
╚════════════════╝ ┴

スクロールコンテナーの中に何らかの要素を挿入したとき、当然スクロールコンテナーの大きさはその分大きくなるわけですが、同時に、スクロールコンテナーに対するスクロールポートの位置も変化することがあります。これは、スクロールポートの外の見えない部分に要素が追加されたときに、スクロールポートで表示する領域を、それまで表示していた領域と合わせるためにオフセットするという挙動です。

例えば、下方向にいくらかスクロールしたことでスクロールコンテナーの上端がスクロールポートから出て、見えなくなっている場面を考えてみます。スクロールコンテナーの縦方向の先頭という見えなくなっている部分に要素を挿入したとき、スクロールコンテナーに対するスクロールポートの位置は、追加した要素の高さだけ下にオフセットされることで、同じ領域を表示し続ける形になります。

╔═════════════════╗ ┘ この位置に
║ Lorem           ║ ┐ 追加したとき、
║ ipsum           ║
╟─────────────────╢
║ dolor           ║
║ sit             ║
╟─────────────────╢
║ amet            ║
╚═════════════════╝

↓

╔═════════════════╗
║ Lipsum Example  ║
║                 ║
║ Lorem           ║
║ ipsum           ║
╟─────────────────╢ ┬
║ dolor           ║ │ スクロールポートはスクロールコンテナーに対し、
║ sit             ║ │ 追加した要素の高さだけ下にオフセットされる。
╟─────────────────╢ ┴
║ amet            ║
╚═════════════════╝

しかし、下方向に少しもスクロールしていなかった場合は、また違った挙動をします。スクロールコンテナー上端がスクロールポートに含まれていたなら、その上端に要素を挿入したとき、スクロールポートの位置は変化しません。これにより、実際の表示内容は、要素の挿入時にガクッと下にズレる挙動をします。

╔═════════════════╗
╟─────────────────╢ ┘ この位置に
║ Lorem           ║ ┐ 追加したとき、
║ ipsum           ║
╟─────────────────╢
║ dolor           ║
║ sit             ║
║ amet            ║
╚═════════════════╝

↓

╔═════════════════╗
╟─────────────────╢ ┬
║ Lipsum Example  ║ │ これまで表示されていたLorem ipsumの部分が
║                 ║ │ 見えなくなってしまった!
╟─────────────────╢ ┴
║ Lorem           ║
║ ipsum           ║
║ dolor           ║
║ sit             ║
║ amet            ║
╚═════════════════╝

このガクッとズレる挙動の不自然さを軽減するためにCSSアニメーションを適用することを考えたときには、次のような順番で処理する必要があります。

  1. 要素を追加する。
  2. 追加した要素の高さ(=スクロールポートのズレの大きさ)を求める。
  3. その高さを打ち消す形でスクロールコンテナーをtransform: translateY()する。
  4. 時間をかけてそのtranslateY()の値を0にする。

実装上問題になるのは、要素は追加してみるまで高さを取得できないこと、アニメーション中にさらに要素が追加されたときにtranslateYの値を適切に合成しなければ連続的なアニメーションにならないこと、などでしょうか。

また、スクロールポートの位置の把握とは別に、スクロールコンテナーの高さの急激な変化についても考慮する必要があります。スクロールコンテナーの高さに影響を受けるスタイルはすべて、この急激な変化に合わせて位置がズレる可能性があります。

これは例えば、タイムラインを表示したメインカラム(要素が追加されるために高さが変化する可能性のある)に対して、サイドカラムが、メインカラムのスクロール量に応じて自動的に表示位置を変える様になっていた場合(2カラムレイアウトだけれどもスクロールバーを1つに抑えるようなデザインをしていた場合)に問題になります。Twitter / XのWebアプリのようなデザインのことです。


ガクッとズレる問題は「DOMのレイアウト座標系」と「ユーザーの視覚的体験」のズレが原因です。translateYで一時的に逆補正した上で、アニメーションで戻していく方式が最も素直な対処となるでしょうか。

Vue.jsで実装するような場合は、要素を追加したあとnextTick()を使って、追加した要素について.getBoundingClientRect().heightを取得し、CSS Animation APIで対処するのがよいでしょう。

また、コンテナーそのものの高さによる外部レイアウトのズレは、外に影響しないoverflow領域に隔離し切り離すことが最も簡単な対処でしょう。つまり、2カラムレイアウトなら素直に2つのoverflow領域を作るべきということです……。

Discussion