交差点オブザーバーを使用した動的ヘッダーの構築

公開: 2022-03-10
簡単な要約↬ビューポート内の特定のしきい値まで、またはビューポート自体の内外でスクロールされるときに、ページ上の一部のコンポーネントが要素に応答する必要があるUIを構築する必要がありましたか? JavaScriptでは、スクロール時にコールバックを常に発生させるようにイベントリスナーをアタッチすると、パフォーマンスが低下する可能性があり、不適切に使用すると、ユーザーエクスペリエンスが低下する可能性があります。 しかし、IntersectionObserverにはもっと良い方法があります。

Intersection ObserverAPIはJavaScriptAPIであり、要素を監視し、スクロールコンテナ内の指定されたポイント(多くの場合(常にではありません))を通過したときにコールバック関数をトリガーすることを検出できます。

Intersection Observerは非同期であるため、メインスレッドでスクロールイベントをリッスンするよりもパフォーマンスが高いと見なすことができます。コールバックは、監視している要素が指定されたしきい値を満たした場合にのみ発生し、スクロール位置が更新されるたびに発生します。 この記事では、Intersection Observerを使用して、Webページのさまざまなセクションと交差するときに変更される固定ヘッダーコンポーネントを構築する方法の例を説明します。

基本的な使用法

Intersection Observerを使用するには、最初に2つのパラメーターを受け取る新しいオブザーバーを作成する必要があります。オブザーバーのオプションを持つオブジェクトと、監視している要素(オブザーバーターゲットと呼ばれる)が交差するたびに実行するコールバック関数です。ルート(ターゲット要素の祖先である必要があるスクロールコンテナ)を使用します。

 const options = { root: document.querySelector('[data-scroll-root]'), rootMargin: '0px', threshold: 1.0 } const callback = (entries, observer) => { entries.forEach((entry) => console.log(entry)) } const observer = new IntersectionObserver(callback, options)

オブザーバーを作成したら、ターゲット要素を監視するようにオブザーバーに指示する必要があります。

 const targetEl = document.querySelector('[data-target]') observer.observe(targetEl)

オプション値はデフォルト値にフォールバックするため、省略できます。

 const options = { rootMargin: '0px', threshold: 1.0 }

ルートが指定されていない場合は、ブラウザのビューポートとして分類されます。 上記のコード例は、 rootMarginthresholdの両方のデフォルト値を示しています。 これらは視覚化するのが難しい場合があるため、説明する価値があります。

rootMargin

rootMargin値は、ルート要素にCSSマージンを追加するのと少し似ています。マージンと同様に、負の値を含む複数の値を取ることができます。 ターゲット要素は、マージンに対して交差していると見なされます。

正と負のルートマージン値を持つスクロールルート。 オレンジ色の正方形は、デフォルトのしきい値を1と想定して、「交差」として分類されるポイントに配置されます。(大きなプレビュー)

つまり、要素が表示されていない場合でも、技術的には「交差」として分類できます(スクロールルートがビューポートの場合)。

オレンジ色の正方形は、表示領域の外側にありますが、ルートと交差しています。 (大プレビュー)

rootMarginデフォルトは0pxですが、CSSのmarginプロパティを使用するのと同じように、複数の値で構成される文字列を受け取ることができます。

threshold

thresholdは、単一の値または0〜1の値の配列で構成できます。これは、交差していると見なされるためにルート境界内にある必要がある要素の比率を表します。 デフォルト値の1を使用すると、ターゲット要素の100%がルート内に表示されたときにコールバックが発生します。

しきい値がそれぞれ1、0、および0.5の場合、100%、0%、および50%が表示されているときにコールバックが起動します。 (大プレビュー)

これらのオプションを使用して、要素がいつ表示可能として分類されるかを視覚化するのは必ずしも簡単ではありません。 交差点オブザーバーを理解するのに役立つ小さなツールを作成しました。

ジャンプした後もっと! 以下を読み続けてください↓

ヘッダーの作成

基本的な原則を理解したので、動的ヘッダーの作成を始めましょう。 まず、セクションに分割されたWebページから始めます。 この画像は、作成するページの完全なレイアウトを示しています。

(大プレビュー)

この記事の最後にデモを含めましたので、コードの選択を解除したい場合は、すぐにデモにジャンプしてください。 (Githubリポジトリもあります。)

各セクションの最小の高さは100vh (ただし、コンテンツによってはさらに長くなる場合があります)。 ヘッダーはページの上部に固定されており、ユーザーがスクロールしても所定の位置に留まります( position: fixedを使用)。 セクションの背景色は異なり、ヘッダーと出会うと、ヘッダーの色が変化してセクションの色を補完します。 ユーザーが現在いるセクションを示すマーカーもあり、次のセクションが到着するとスライドします。 関連するコードに直接アクセスできるようにするために、(Intersection Observer APIの使用を開始する前に)開始点を使用して最小限のデモを設定しました。

マークアップ

ヘッダーのHTMLから始めます。 これは、ホームリンクとナビゲーションを備えた非常に単純なヘッダーになりますが、特に凝ったものはありませんが、いくつかのデータ属性を使用します。ヘッダー自体のdata-headerヘッダー(JSで要素をターゲットにできるようにするため) 、および属性data-linkを持つ3つのアンカーリンク。クリックすると、ユーザーを関連するセクションにスクロールします。

 <header data-header> <nav class="header__nav"> <div class="header__left-content"> <a href="#0">Home</a> </div> <ul class="header__list"> <li> <a href="#about-us" data-link>About us</a> </li> <li> <a href="#flavours" data-link>The flavours</a> </li> <li> <a href="#get-in-touch" data-link>Get in touch</a> </li> </ul> </nav> </header>

次に、セクションに分割されたページの残りの部分のHTML。 簡潔にするために、記事に関連する部分のみを含めましたが、完全なマークアップはデモに含まれています。 各セクションには、背景色の名前を指定するデータ属性と、ヘッダーのアンカーリンクの1つに対応するidが含まれています。

 <main> <section data-section="raspberry"> <!--Section content--> </section> <section data-section="mint"> <!--Section content--> </section> <section data-section="vanilla"> <!--Section content--> </section> <section data-section="chocolate"> <!--Section content--> </section> </main>

ユーザーがスクロールしてもページの上部に固定されるように、CSSを使用してヘッダーを配置します。

 header { position: fixed; width: 100%; }

また、セクションに最小の高さを与え、コンテンツを中央に配置します。 (このコードは、交差点オブザーバーが機能するために必要ではありません。設計のためだけです。)

 section { padding: 5rem 0; min-height: 100vh; display: flex; justify-content: center; align-items: center; }

iframeの警告

このCodepenデモを作成しているときに、完全に機能するはずのIntersection Observerコードが交差点の正しいポイントでコールバックを起動できず、代わりにターゲット要素がビューポートのエッジと交差したときに起動するという厄介な問題に遭遇しました。 少し頭を悩ませた後、これはCodepenではコンテンツがiframe内に読み込まれ、処理が異なるためであることに気付きました。 (詳細については、クリッピングと交差長方形に関するMDNドキュメントのセクションを参照してください。)

回避策として、デモでは、マークアップを別の要素でラップできます。これは、予想どおり、ブラウザーのビューポートではなく、スクロールコンテナー(IOオプションのルート)として機能します。

 <div class="scroller" data-scroller> <header data-header> <!--Header content--> </header> <main> <!--Sections--> </main> </div>

同じデモの代わりにビューポートをルートとして使用する方法を確認したい場合は、これがGithubリポジトリに含まれています。

CSS

CSSでは、使用している色のカスタムプロパティをいくつか定義します。 また、ヘッダーテキストと背景色の2つの追加のカスタムプロパティを定義し、いくつかの初期値を設定します。 (これらの2つのカスタムプロパティは、後で異なるセクション用に更新します。)

 :root { --mint: #5ae8d5; --chocolate: #573e31; --raspberry: #f2308e; --vanilla: #faf2c8; --headerText: var(--vanilla); --headerBg: var(--raspberry); }

ヘッダーで次のカスタムプロパティを使用します。

 header { background-color: var(--headerBg); color: var(--headerText); }

また、さまざまなセクションの色も設定します。 私はセレクターとしてデータ属性を使用していますが、必要に応じてクラスを簡単に使用することもできます。

 [data-section="raspberry"] { background-color: var(--raspberry); color: var(--vanilla); } [data-section="mint"] { background-color: var(--mint); color: var(--chocolate); } [data-section="vanilla"] { background-color: var(--vanilla); color: var(--chocolate); } [data-section="chocolate"] { background-color: var(--chocolate); color: var(--vanilla); }

各セクションが表示されているときに、ヘッダーにいくつかのスタイルを設定することもできます。

 /* Header */ [data-theme="raspberry"] { --headerText: var(--raspberry); --headerBg: var(--vanilla); } [data-theme="mint"] { --headerText: var(--mint); --headerBg: var(--chocolate); } [data-theme="chocolate"] { --headerText: var(--chocolate); --headerBg: var(--vanilla); }

各交差点でヘッダーのdata-theme属性を切り替えるため、ここでデータ属性を使用する場合のより強力なケースがあります。

オブザーバーの作成

ページの基本的なHTMLとCSSを設定したので、各セクションが表示されるのを監視するオブザーバーを作成できます。 ページを下にスクロールしているときに、セクションがヘッダーの下部に接触するたびにコールバックを発生させたいと考えています。 これは、ヘッダーの高さに対応する負のルートマージンを設定する必要があることを意味します。

 const header = document.querySelector('[data-header]') const sections = [...document.querySelectorAll('[data-section]')] const scrollRoot = document.querySelector('[data-scroller]') const options = { root: scrollRoot, rootMargin: `${header.offsetHeight * -1}px`, threshold: 0 }

セクションのいずれかの部分がルートマージンと交差している場合に起動するように、しきい値を0に設定しています。

まず、ヘッダーのdata-theme値を変更するためのコールバックを作成します。 (これは、クラスを追加および削除するよりも簡単です。特に、ヘッダー要素に他のクラスが適用されている場合はそうです。)

 /* The callback that will fire on intersection */ const onIntersect = (entries) => { entries.forEach((entry) => { const theme = entry.target.dataset.section header.setAttribute('data-theme', theme) }) }

次に、交差するセクションを監視するオブザーバーを作成します。

 /* Create the observer */ const observer = new IntersectionObserver(onIntersect, options) /* Set our observer to observe each section */ sections.forEach((section) => { observer.observe(section) })

これで、各セクションがヘッダーに一致すると、ヘッダーの色が更新されるはずです。

ミシェル・バーカーのペン[ハッピーフェイスアイスクリームパーラー–ステップ2](https://codepen.io/smashingmag/pen/poPgpjZ)をご覧ください。

ペンハッピーフェイスアイスクリームパーラー–ミシェルバーカーによるステップ2を参照してください。

ただし、下にスクロールすると、色が正しく更新されないことに気付く場合があります。 実際、ヘッダーは前のセクションの色で毎回更新されています! 一方、上にスクロールすると、完全に機能します。 スクロール方向を決定し、それに応じて動作を変更する必要があります。

スクロール方向を見つける

JSに、スクロールの方向に変数を設定します。初期値は'up'で、別の変数は最後の既知のスクロール位置( prevYPosition )に設定します。 次に、コールバック内で、スクロール位置が前の値よりも大きい場合、 directionの値を'down'または'up'場合も同様です。

 let direction = 'up' let prevYPosition = 0 const setScrollDirection = () => { if (scrollRoot.scrollTop > prevYPosition) { direction = 'down' } else { direction = 'up' } prevYPosition = scrollRoot.scrollTop } const onIntersect = (entries, observer) => { entries.forEach((entry) => { setScrollDirection() /* ... */ }) }

また、ヘッダーの色を更新する新しい関数を作成し、ターゲットセクションを引数として渡します。

 const updateColors = (target) => { const theme = target.dataset.section header.setAttribute('data-theme', theme) } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() updateColors(entry.target) }) }

これまでのところ、ヘッダーの動作に変更はありません。 しかし、スクロール方向がわかったので、 updateColors()関数に別のターゲットを渡すことができます。 スクロール方向が上向きの場合は、エントリターゲットを使用します。 ダウンしている場合は、次のセクションを使用します(ある場合)。

 const getTargetSection = (target) => { if (direction === 'up') return target if (target.nextElementSibling) { return target.nextElementSibling } else { return target } } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() const target = getTargetSection(entry.target) updateColors(target) }) }

ただし、もう1つ問題があります。セクションがヘッダーにヒットしたときだけでなく、ビューポートの下部に次の要素が表示されたときにヘッダーが更新されます。 これは、オブザーバーがコールバックを2回起動するためです。1回は要素が入るときに、もう1回は要素が出るときに発生します。

ヘッダーを更新する必要があるかどうかを判断するには、 entryオブジェクトのisIntersectingキーを使用できます。 ヘッダーの色を更新するかどうかのブール値を返す別の関数を作成してみましょう。

 const shouldUpdate = (entry) => { if (direction === 'down' && !entry.isIntersecting) { return true } if (direction === 'up' && entry.isIntersecting) { return true } return false }

それに応じてonIntersect()関数を更新します。

 const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() /* Do nothing if no need to update */ if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) }) }

これで、色が正しく更新されるはずです。 CSSトランジションを設定して、効果を少し良くすることができます。

 header { transition: background-color 200ms, color 200ms; } 

ミシェル・バーカーのペン[ハッピーフェイスアイスクリームパーラー–ステップ3](https://codepen.io/smashingmag/pen/bGWEaEa)をご覧ください。

ペンハッピーフェイスアイスクリームパーラー–ミシェルバーカーによるステップ3を参照してください。

動的マーカーの追加

次に、ヘッダーにマーカーを追加して、さまざまなセクションにスクロールするとその位置を更新します。 これには疑似要素を使用できるため、HTMLに何も追加する必要はありません。 ヘッダーの左上に配置するための簡単なCSSスタイルをいくつか与え、背景色を付けます。 これにはcurrentColorを使用しています。これは、ヘッダーテキストの色の値を使用するためです。

 header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; background-color: currentColor; }

幅にはカスタムプロパティを使用でき、デフォルト値は0です。また、変換x値にもカスタムプロパティを使用します。 ユーザーがスクロールするときに、コールバック関数でこれらの値を設定します。

 header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; width: var(--markerWidth, 0); background-color: currentColor; transform: translate3d(var(--markerLeft, 0), 0, 0); }

これで、交点でのマーカーの幅と位置を更新する関数を作成できます。

 const updateMarker = (target) => { const id = target.id /* Do nothing if no target ID */ if (!id) return /* Find the corresponding nav link, or use the first one */ let link = headerLinks.find((el) => { return el.getAttribute('href') === `#${id}` }) link = link || headerLinks[0] /* Get the values and set the custom properties */ const distanceFromLeft = link.getBoundingClientRect().left header.style.setProperty('--markerWidth', `${link.clientWidth}px`) header.style.setProperty('--markerLeft', `${distanceFromLeft}px`) }

色を更新すると同時に関数を呼び出すことができます。

 const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) updateMarker(target) }) }

また、マーカーの初期位置を設定する必要があるため、どこからともなく表示されるだけではありません。 ドキュメントが読み込まれると、最初のセクションをターゲットとして使用して、 updateMarker()関数を呼び出します。

 document.addEventListener('readystatechange', e => { if (e.target.readyState === 'complete') { updateMarker(sections[0]) } })

最後に、CSSトランジションを追加して、マーカーがヘッダーを横切って1つのリンクから次のリンクにスライドするようにします。 widthプロパティを移行しているときに、 will-changeを使用して、ブラウザーが最適化を実行できるようにします。

 header::after { transition: transform 250ms, width 200ms, background-color 200ms; will-change: width; }

スムーズなスクロール

最後の仕上げとして、ユーザーがリンクをクリックしたときに、セクションにジャンプするのではなく、ページをスムーズに下にスクロールすると便利です。 最近では、CSSで正しく実行できます。JSは必要ありません。 よりアクセスしやすいエクスペリエンスを実現するには、システム設定でモーションの削減の設定を指定していない場合にのみスムーズなスクロールを実装することで、ユーザーのモーションの設定を尊重することをお勧めします。

 @media (prefers-reduced-motion: no-preference) { .scroller { scroll-behavior: smooth; } }

最終デモ

上記のすべての手順をまとめると、完全なデモが作成されます。

ミシェル・バーカーのペン[ハッピーフェイスアイスクリームパーラー–交差点オブザーバーの例](https://codepen.io/smashingmag/pen/XWRXVXQ)を参照してください。

ミシェルバーカーによるペンハッピーフェイスアイスクリームパーラー–交差点オブザーバーの例を参照してください。

ブラウザのサポート

Intersection Observerは、最新のブラウザで広くサポートされています。 必要に応じて、古いブラウザ用にポリフィルすることができますが、可能な場合はプログレッシブエンハンスメントアプローチを採用することを好みます。 私たちのヘッダーの場合、サポートされていないブラウザーにシンプルで変更されていないバージョンを提供することは、ユーザーエクスペリエンスに大きな悪影響を与えることはありません。

Intersection Observerがサポートされているかどうかを検出するには、次を使用できます。

 if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) { /* Code to execute if IO is supported */ } else { /* Code to execute if not supported */ }

資力

交差点オブザーバーについてもっと読む:

  • MDNからのいくつかの実用的な例を含む広範なドキュメント
  • 交差点オブザーバービジュアライザーツール
  • Intersection Observer APIを使用したタイミング要素の可視性– MDNの別のチュートリアルで、IOを使用して広告の可視性を追跡する方法を説明します
  • Denys Mishunovによるこの記事では、アセットの遅延読み込みなど、IOのその他の用途について説明します。 これは今ではそれほど必要ではありませんが( loading属性のおかげで)、ここで学ぶことはまだたくさんあります。