CSS position:sticky イベント

要約

ヒント: 次のアプリでは、scroll イベントは必要ないかもしれません。使用 IntersectionObserver, position:sticky 要素が固定されたとき、または固定されなくなったときにカスタム イベントを起動する方法を紹介します。すべて スクロール リスナーの使用。それを実証する素晴らしいデモもあります。

<ph type="x-smartling-placeholder">
</ph>
デモを見る | 出典

sticky-change イベントの導入

CSS のスティッキー ポジションを使用する際の実際の制限事項の一つは、プロパティがアクティブになっていることを知るためのプラットフォーム シグナルが提供されないことです。つまり、要素が固定状態になったときや、固定されたときなど、 固定されます

次の例では、画像から 10 ピクセルの <div class="sticky"> を固定しています。 その親コンテナの最上位に置くことができます。

.sticky {
  position: sticky;
  top: 10px;
}

要素がマークに達したときにブラウザがそれを認識すると、便利です。 私以外にも、そう考える人がいるようです。position:sticky のシグナルにより、次のようなユースケースが実現できる可能性があります。

  1. バナーが貼り付くようにドロップ シャドウを適用します。
  2. ユーザーがコンテンツを読んだら、アナリティクスのヒットを記録して、ユーザーの情報を把握します。 できます。
  3. ユーザーがページをスクロールしたら、フローティング TOC ウィジェットを現在のものに更新する できます。

こうしたユースケースを念頭に置き、Google は、 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;
});

デモでは、このイベントを使用して、固定されたときにヘッダーにドロップシャドウを適用しています。また、 新しいタイトルを付けます。

デモでは、scrollevents なしでエフェクトが適用されます。

スクロール イベントのないスクロール エフェクト

<ph type="x-smartling-placeholder">
</ph> ページの構造。
ページの構造

以降の投稿でこれらの名前を参照できるように、まずは用語を整理しておきましょう。

  1. スクロール コンテナ - 「ブログ投稿」のリストを含むコンテンツ領域(表示ビューポート)。
  2. ヘッダー - position:sticky が付いている各セクションの青いタイトル。
  3. 固定セクション - 各コンテンツ セクション。固定ヘッダーの下にスクロールするテキスト。
  4. 「固定モード」 - position:sticky が要素に適用される場合。

どのヘッダーが「固定モード」になっているかを判断するには、なんらかの方法で スクロール コンテナのスクロール オフセット。これにより、現在表示されているヘッダーを計算できるようになります。ただし、 scroll イベントなしで行うのは簡単ではありません。 position:sticky は、要素が固定されるとレイアウトから要素を削除します。

そのため、スクロール イベントがないと、ヘッダーでレイアウト関連の計算を実行できなくなります

スクロール位置を決定するためにダンビー DOM を追加する

ここでは、scroll イベントの代わりに IntersectionObserver を使用して、 ヘッダーの固定モードの開始と終了を指定します。各固定セクションに 2 つのノード(センチネル)を追加します(上部に 1 つ、下部に 1 つ)。これらは、スクロール位置を特定するためのウェイポイントとして機能します。これらの マーカーがコンテナに出入りすると、表示が変化し、 Intersection Observer がコールバックを開始します。

<ph type="x-smartling-placeholder">
</ph> センチネル要素が表示されていない場合
隠れたセンチネル要素。

上下にスクロールする 4 つのケースに対応するには、2 つの見解が必要です。

  1. 下にスクロールする - 上部のセンチネルがコンテナの上部を越えると、ヘッダーが固定されます。
  2. 下にスクロールする - ヘッダーは、セクションの下部に達し、下部センチネルがコンテナの上部を越えると、固定モードを終了します。
  3. 上にスクロール - ヘッダーは最上部をスクロールすると固定モードのままになります ビューに戻ります
  4. 上方向へのスクロール - 下部のセンチネルが上部から再びビュー内に入ると、ヘッダーが固定されます。

1 ~ 4 個のスクリーンキャストを発生順に表示しておくと便利です。

<ph type="x-smartling-placeholder">
</ph>
Intersection Observer は、警戒心が スクロール コンテナに入る/出る。

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 は、すべての交差点の変化を非同期的に監視します。 ターゲット要素とドキュメント ビューポートまたは親コンテナの 2 つのいずれかになります。この例では、親コンテナとの交差を監視します。

魔法のソースは 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 関数は、センチネル ノードを作成して各セクションに接続します。オブザーバーは、センチネルとコンテナの底との交差点を計算し、コンテナの外側に入るか外側から入ってくるかを判断します。この情報により、セクション ヘッダーが 気づくかもしれません。

/**
 * 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 イベントを使用せずにスクロール エフェクトを追加しました。

デモを見る | ソース

まとめ

よく疑問に思ったことがあります。IntersectionObserver は イベントベースの scroll の UI パターンを置き換えるのに役立つツールです。 長年にわたって進化してきました答えは「はい」と「いいえ」です。IntersectionObserver API のセマンティクスにより、すべての用途に使用するのは困難です。ただし、ここで説明したように、興味深いテクニックに使用できます。

スタイルの変更を検出する別の方法はありますか?

そうでもありません。必要なのは、DOM 要素のスタイル変更を監視する方法でした。 残念ながら、スタイルの変更を監視できるウェブ プラットフォーム API はありません。

MutationObserver が最初の選択肢として考えられますが、ほとんどの場合、これは機能しません。たとえば、デモでは、sticky クラスが要素に追加されたときにコールバックを受け取りますが、要素の計算スタイルが変更されたときにはコールバックを受け取りません。sticky クラスはページ読み込み時にすでに宣言されていることを思い出してください。

今後、要素の計算スタイルの変更を監視するために、Mutation Observer の「Style Mutation Observer」拡張機能が役立つ可能性があります。position: sticky