Web production note

 【更新日 :

【CSS, JavaScript】スクロールしてエリアが切り替わる時に次のコンテンツと重ねる

Category:
css / js

JavaScriptとposition: sticky; を用いてスクロール時にエリア同士を重ねる実装のサンプルです。

動作サンプル

codepen

See the Pen Overlap areas with scrolling Vanilla JS (position: sticky;) by web_corder (@web_walking_nak) on CodePen.

以下は最低限動作させるための設定方法です。

HTML

複数エリアの繰り返しや、間に設定しない要素を挟んでも動作します。

<div class="js-scroll-overlap">
  <p>重ねるコンテンツの数だけ生成</p>
</div>
<div class="js-scroll-overlap">
  <p>重ねるコンテンツの数だけ生成</p>
</div>

<div class="relative">
  <p>重なる演出をしない場所では、position:relative;のみ設定</p>
</div>

<div class="js-scroll-overlap">
  <p>重ねるコンテンツの数だけ生成</p>
</div>
<div class="js-scroll-overlap">
  <p>重ねるコンテンツの数だけ生成</p>
</div>

設定

  • 重ねる効果を与えるエリアに class="js-scroll-overlap" を追加。
  • 重なる演出をしない場所では、position: relative; のみ設定。

仕組み的には一番最後の要素の場合(コンテンツ途中で重なる演出をしない場合は別)、position: relative; の切り替えをせず、全ての要素に class=”js-scroll-overlap” を追加しても動作しますが、その分JavaScript側で余分な処理が増えてしまうため可能な限りマークアップで切り替えることを推奨します。

レイアウトシフトが発生するマークアップの場合は上手く処理できない

コンテンツにimgタグを内包させる場合、レイアウトシフトが発生する書き方をしているとエリアの高さが正しく取得できない場合があります。
動作がおかしいと感じる時はimgタグにwidth属性とheight属性が明記されているかなど、レイアウトシフトへの対応ができているか確認してください。

レイアウトシフトの参考サイト

入れ子構造の場合

目的の挙動になるかは内包するコンテンツ次第で多少変化しますが、入れ子構造でも動作します。
<div class="js-scroll-overlap">
  <div class="js-scroll-overlap">
    <p>重ねるコンテンツの数だけ生成</p>
  </div>
  <div class="js-scroll-overlap">
    <p>重ねるコンテンツの数だけ生成</p>
  </div>

  <div class="relative">
    <p>重なる演出をしない場所では、position:relative;のみ設定</p>
  </div>
</div>

<div class="relative">
  <p>重なる演出をしない場所では、position:relative;のみ設定</p>
</div>

CSS

position: relative; のclassは各環境に合わせて必要に応じて準備してください。

.js-scroll-overlap:not(.is-disabled) {
  --sticky-offset: -1px;
  position: sticky;
  top: var(--sticky-offset);
}
.relative {
  position: relative;
}

is-disabled はキーボードフォーカス時に動作を無効化する際に付与されます。
※フォーカス移動時にフォーカスした要素と、次のエリアが重なって要素が見えなくなってしまう可能性への対応です。

JavaScript

//スクロールしてエリアが切り替わる時に次のコンテンツと重ねる
function scrollOverlap() {
  const targets = document.querySelectorAll('.js-scroll-overlap');
  if (targets.length === 0) {
    return;
  }

  let lastWinHight = window.innerHeight;

  //position: sticky;のオフセット値をCSS変数で設定
  const setStickyOffset = () => {
    targets.forEach((target) => {
      const targetHeight = target.offsetHeight;
			//ウィンドウの高さが要素の高さより大きい場合はオフセット値を-1pxに設定
      const offsetValue = lastWinHight > targetHeight ? '-1px' : `-${targetHeight - lastWinHight}px`;
      target.style.setProperty('--sticky-offset', offsetValue);
    });
  };
  setStickyOffset();

  addEventListener('resize', () => {
	  //ウィンドウの高さが変わった時のみウィンドウの高さを更新
    const winHight = window.innerHeight;
    if (lastWinHight !== winHight) {
      lastWinHight = winHight;
    }
  });

  // bodyのサイズ変更を監視して再取得
  const body = document.body;
  // ResizeObserverを作成
  const resizeObserver = new ResizeObserver((entries) => {
    for (let entry of entries) {
      setStickyOffset();
    }
  });
  // body要素を監視対象に追加
  resizeObserver.observe(body);

  /*
    キーボード操作時に無効化するクラスを付与する
     フォーカス移動時にフォーカスした要素と、
		 次のエリアが重なって要素が見えなくなってしまう可能性への対応
  */
  const toggleDisabledClass = (boolean) => {
    targets.forEach((target) => {
      target.classList.toggle('is-disabled', boolean);
    });
  };

  //キーボードフォーカス状態を管理するフラグ
  let isKeyboardFocus = false;

  //キーボードのフォーカス移動時には無効化する
  document.addEventListener('keydown', (event) => {
    if (event.key === 'Tab') {
      //Tabキーが押されたのでキーボード操作中
      isKeyboardFocus = true; 
      toggleDisabledClass(isKeyboardFocus);
    }
  });

  //マウス操作の検知で無効化を解除する
  document.addEventListener('mousedown', () => {
    if (isKeyboardFocus) {
      //マウス操作があったのでキーボードフォーカスを解除
      isKeyboardFocus = false;
      toggleDisabledClass(isKeyboardFocus);
    }
  });
}

関数の呼び出し

任意の場所で以下を実行し関数を呼び出してください。

scrollOverlap();
目次 を閉じる