要約
次のアプリでは scroll
イベントは必要ないかもしれません。IntersectionObserver
を使用して、position:sticky
要素が固定されたとき、または固定されなくなったときにカスタム イベントを発生させる方法について説明します。スクロールリスナーを
使用する必要はありませんそれを実証する素晴らしいデモもあります。
sticky-change
イベントのご紹介
CSS 固定位置を使用する場合の実用的な制限の 1 つは、プロパティがアクティブになったことを知らせるプラットフォーム シグナルが提供されないことです。つまり、要素が固定状態になったタイミングや、要素が固定されなくなったタイミングを把握するためのイベントはありません。
次の例では、<div class="sticky">
が親コンテナの上部から 10 ピクセル固定されています。
.sticky {
position: sticky;
top: 10px;
}
要素がマークに達したときにブラウザがそれを認識すると、便利です。
そう考えているのは私だけではないようです。position:sticky
のシグナルによって、さまざまなユースケースを実現できます。
- バナーが固定されたら、ドロップ シャドウを適用します。
- ユーザーがコンテンツを読んだら、アナリティクス ヒットを記録して進行状況を確認します。
- ユーザーがページをスクロールしたら、フローティング TOC ウィジェットを現在のセクションに更新します。
こうしたユースケースを念頭に置き、position:sticky
要素が修正されたときに発生するイベントを作成するという最終目標を作成しました。これを sticky-change
イベントとします。
document.addEventListener('sticky-change', e => {
const header = e.detail.target; // header became sticky or stopped sticking.
const sticking = e.detail.stuck; // true when header is sticky.
header.classList.toggle('shadow', sticking); // add drop shadow when sticking.
document.querySelector('.who-is-sticking').textContent = header.textContent;
});
デモでは、このイベントを使用して、ドロップ シャドウが修正されたときにドロップ シャドウをヘッダーで設定します。また、ページ上部の新しいタイトルも更新されます。
スクロール イベントのないスクロール効果
この投稿の残りの部分でこれらの名前を参照できるように、いくつかの用語を取り除いていきましょう。
- スクロール コンテナ - 「ブログ投稿」のリストを含むコンテンツ領域(表示ビューポート)。
- ヘッダー -
position:sticky
がある各セクションの青色のタイトル。 - 固定セクション - 各コンテンツ セクション。固定ヘッダーの下でスクロールするテキスト。
- 「固定モード」 -
position:sticky
が要素に適用される場合。
「固定モード」に入るヘッダーを確認するには、スクロール コンテナのスクロール オフセットを特定する方法が必要です。これにより、現在表示されている header を計算できます。ただし、scroll
イベントなしで行うのはかなり厄介です。もう一つの問題は、position:sticky
が修正されるとレイアウトから要素を削除することです。
そのため、スクロール イベントがないと、ヘッダーでレイアウト関連の計算を実行する機能が失われます。
スクロール位置を決定するためにダンビー DOM を追加する
scroll
イベントの代わりに、IntersectionObserver
を使用して、headersが固定モードを開始するタイミングと終了するタイミングを決定します。各固定セクションに 2 つのノード(標識)を追加すると、スクロール位置を把握するためのウェイポイントとして機能します。これらのマーカーがコンテナに出入りすると表示が変化し、Intersection Observer がコールバックを開始します。
上下にスクロールする 4 つのケースに対応するには、2 つの見解が必要です。
- 下にスクロール - ヘッダーは、最上位のセンチネルがコンテナの上部を横断すると固定されます。
- 下にスクロール - header は、セクションの下部に到達し、下部の標識がコンテナの上部を横切るため、固定モードを終了します。
- 上へのスクロール - header は、最上部のスクロールで上からビューに戻ると固定モードを終了します。
- 上へのスクロール - ヘッダーは、下部のセンチネルが上からビューに戻ると固定されます。
1 ~ 4 個のスクリーンキャストを発生順に表示しておくと便利です。
CSS
標識は各セクションの上部と下部に配置されます。.sticky_sentinel--top
はヘッダーの最上部に配置され、.sticky_sentinel--bottom
はセクションの下部に配置されます。
:root {
--default-padding: 16px;
--header-height: 80px;
}
.sticky {
position: sticky;
top: 10px; /* adjust sentinel height/positioning based on this position. */
height: var(--header-height);
padding: 0 var(--default-padding);
}
.sticky_sentinel {
position: absolute;
left: 0;
right: 0; /* needs dimensions */
visibility: hidden;
}
.sticky_sentinel--top {
/* Adjust the height and top values based on your on your sticky top position.
e.g. make the height bigger and adjust the top so observeHeaders()'s
IntersectionObserver fires as soon as the bottom of the sentinel crosses the
top of the intersection container. */
height: 40px;
top: -24px;
}
.sticky_sentinel--bottom {
/* Height should match the top of the header when it's at the bottom of the
intersection container. */
height: calc(var(--header-height) + var(--default-padding));
bottom: 0;
}
Intersection Observer を設定する
Intersection Observer は、ターゲット要素とドキュメント ビューポートまたは親コンテナとの交差点の変化を非同期で監視します。この例では、親コンテナとの交差点を確認できます。
マジック ソースは IntersectionObserver
です。各標識は IntersectionObserver
を取得し、スクロール コンテナ内の交差点の表示を観察します。センチネルが表示可能なビューポートまでスクロールすると、ヘッダーが固定されるか、固定されなくなったことが分かります。同様に 標識がビューポートを出たとき
まず、ヘッダーとフッターのセンチネルのオブザーバーを設定します。
/**
* Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
* Note: the elements should be children of `container`.
* @param {!Element} container
*/
function observeStickyHeaderChanges(container) {
observeHeaders(container);
observeFooters(container);
}
observeStickyHeaderChanges(document.querySelector('#scroll-container'));
次に、.sticky_sentinel--top
要素がスクロール コンテナの上部(いずれかの方向に)を通過したときに起動するオブザーバーを追加しました。observeHeaders
関数は、トップ センチネルを作成して各セクションに追加します。オブザーバーは、センチネルとコンテナの頂点の交差を計算し、コンテナがビューポートに入るか外に出るかを判断します。この情報により、セクション ヘッダーが固定されるかどうかが決まります。
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--top` become visible/invisible at the top of the container.
* @param {!Element} container
*/
function observeHeaders(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
// Started sticking.
if (targetInfo.bottom < rootBoundsInfo.top) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.bottom >= rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [0], root: container});
// Add the top sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--top');
sentinels.forEach(el => observer.observe(el));
}
オブザーバーは threshold: [0]
で構成されているため、センチネルが可視化されるとすぐにコールバックが呼び出されます。
一番下の文(.sticky_sentinel--bottom
)についても同様です。フッターがスクロール コンテナの下部を通過すると起動する 2 つ目のオブザーバーが作成されます。observeFooters
関数は、sentinel ノードを作成し、各セクションにアタッチします。オブザーバーは、センチネルとコンテナの底部の交差を計算し、コンテナの出入りを判断します。この情報により、セクション ヘッダーが固定されるかどうかが決まります。
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
* container.
* @param {!Element} container
*/
function observeFooters(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
const ratio = record.intersectionRatio;
// Started sticking.
if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.top < rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [1], root: container});
// Add the bottom sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
sentinels.forEach(el => observer.observe(el));
}
オブザーバーは threshold: [1]
で構成されているため、ノード全体がビュー内にあるときにコールバックが発生します。
最後に、sticky-change
カスタム イベントを発生させてセンチネルを生成するためのユーティリティが 2 つあります。
/**
* @param {!Element} container
* @param {string} className
*/
function addSentinels(container, className) {
return Array.from(container.querySelectorAll('.sticky')).map(el => {
const sentinel = document.createElement('div');
sentinel.classList.add('sticky_sentinel', className);
return el.parentElement.appendChild(sentinel);
});
}
/**
* Dispatches the `sticky-event` custom event on the target element.
* @param {boolean} stuck True if `target` is sticky.
* @param {!Element} target Element to fire the event on.
*/
function fireEvent(stuck, target) {
const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
document.dispatchEvent(e);
}
これで作業は完了です。
最後のデモ
position:sticky
を含む要素が固定されたときに発生するカスタム イベントを作成し、scroll
イベントを使用せずにスクロール効果を追加しました。
おわりに
長年にわたって開発されてきた scroll
イベントベースの UI パターンの一部を置き換えるツールとして IntersectionObserver
が役立つのかとよく疑問に思っています。答えは「はい」か「いいえ」です。IntersectionObserver
API のセマンティクスにより、すべてに使用することは困難です。しかし、ここで紹介したように、いくつかの興味深い手法に使用できます。
スタイルの変更を検出する別の方法は?
そうでもありません。必要なのは、DOM 要素のスタイル変更を監視する方法でした。残念ながら、ウェブ プラットフォーム API には、スタイルの変更を監視できるものはありません。
MutationObserver
が第 1 の選択肢となりますが、これはほとんどのケースでは機能しません。たとえば、このデモでは、sticky
クラスが要素に追加されたときにコールバックを受け取りますが、要素の計算済みスタイルが変更されたときにはコールバックを受け取りません。sticky
クラスはページの読み込み時にすでに宣言されていることを思い出してください。
将来的には、要素の計算済みスタイルの変更を監視するために、Mutation Observer の「スタイル ミューテーション オブザーバー」拡張機能が役立つ可能性があります。position: sticky