スクロールでセクションが切り替わる時に次のコンテンツと重ねる実装まとめposition stickyとVanilla JSで作る 2026年の実務向けレシピ

Web技術

「スクロールしていくと、次のセクションが前のセクションにぬるっと重なって切り替わる」。 この見せ方、LPやブランドサイトでよく見ます。派手すぎないのに雰囲気が出る。しかも、意外と離脱も減る。

ただし実装は、見た目よりも少しだけ気を使います。 雑に作ると、スマホでずれたり、画像が入った瞬間に高さが変わって破綻したり、Tabキーで移動したらフォーカスが隠れたり。 現場あるあるが詰まっています。

この記事では、下記の内容をベースにしつつ、実務で困りにくい形へ噛み砕いてまとめ直します。:contentReference[oaicite:0]{index=0}

  • position: stickyを使って「重なり」を作る考え方
  • 要素の高さとウィンドウ高さからオフセットを計算するコツ
  • 画像やフォントでレイアウトがズレる問題の対策
  • キーボード操作でのアクセシビリティ事故を避ける方法
  • オーバーレイ演出(暗幕)を追加する応用
  • WordPressでも崩れにくい設計とデバッグ手順

忙しい人向けに先に結論を言うと、勝ち筋はこうです。

  • 重ねたい各セクションは「sticky」で画面に張り付かせる
  • ただし「top」は固定値でなく、要素の高さに応じてCSS変数で動的に決める
  • bodyのサイズ変化(画像読み込みなど)を監視して再計算する
  • Tab移動の時は動きを止めて、フォーカスが隠れないようにする

この4つを押さえると、だいたいの現場で安定します。

  1. この表現は何がうれしいのか
    1. スクロール演出の中でもコスパが良い
    2. ユーザーの体験を邪魔しにくい
  2. 仕組みの核心 position stickyで重ねる
    1. stickyは親要素の中で張り付く
    2. topを固定値にしないのがポイント
  3. 最小構成 まずは通常版を作る
    1. HTML 重ねたいセクションに同じクラスを付ける
    2. CSS stickyのtopをCSS変数で制御する
    3. JavaScript 要素の高さとウィンドウ高さでoffsetを計算する
  4. 実務でハマるポイントを先に潰す
    1. 親や祖先にoverflow hiddenがあるとstickyが死ぬ
    2. 画像のレイアウトシフトで高さが狂う
    3. フォーカスが隠れる問題は必ず意識する
  5. 応用 オーバーレイ演出を入れて切り替えを強調する
    1. やりたいことは暗幕をかけて切り替え感を出す
    2. HTML data-is-overlayで有効化する
    3. CSS afterで暗幕を描き CSS変数で透明度を操作する
    4. JS スクロール位置から透明度を線形に計算する
  6. 設計をもう一段強くする 実務向けの改善案
    1. スクロールイベントは出来ればまとめて管理する
    2. prefers-reduced-motion reduce の時は演出を弱める
    3. スマホのアドレスバー挙動に注意する
  7. WordPressで使う時の現実的なやり方
    1. ブロックの増減があっても動くマークアップにする
    2. 画像は必ずサイズを持たせる
  8. デバッグ手順 動かない時に見る順番
    1. 1 stickyが効いているか
    2. 2 CSS変数 –sticky-offset が更新されているか
    3. 3 高さが正しく取れているか
    4. 4 Tab移動でおかしくなる場合
  9. 関連して知っておくと得をする話
    1. stickyは万能ではないが味方にすると強い
    2. IntersectionObserverとResizeObserverは現代の現場装備
    3. 演出は足し算より引き算が大事
  10. まとめ 速く作れて 事故りにくい重ねスクロールの作り方

この表現は何がうれしいのか

スクロール演出の中でもコスパが良い

フルスクリーンの派手なパララックスは、作るのも重いし、端末によって印象が変わりやすいです。 それに比べて「セクションを重ねて切り替える」は、動きの質感が出る割に実装が軽い。

しかも、コンテンツが普通に縦に並んでいる構造を大きく壊しにくいので、デザイン変更にも強いです。

ユーザーの体験を邪魔しにくい

スクロールを奪わない。勝手に横移動もしない。勝手にスナップもしない。 なので、ストレスを増やしにくいタイプの演出です。

ただし、アクセシビリティだけは油断すると一瞬で事故ります。 後半でしっかり潰します。

仕組みの核心 position stickyで重ねる

stickyは親要素の中で張り付く

position: stickyは、要素が一定の位置に来ると、親の範囲内で固定される挙動です。 この特性を使って、複数のセクションを同じ場所に張り付かせることで「重なり」を作ります。:contentReference[oaicite:1]{index=1}

ただし大事な注意点があります。 stickyは、祖先要素に overflow: hidden などがあると動かないことがあります。:contentReference[oaicite:2]{index=2} 現場で「動かないんだけど」案件の半分くらいはこれです。 まず疑ってください。

topを固定値にしないのがポイント

よくあるsticky実装は top: 0 で済ませます。 でも今回の演出は、セクションの高さがウィンドウより大きい時と小さい時で、見せたい振る舞いが変わります。

例えば、要素の高さがウィンドウより小さいなら、ほぼ top: 0 でよい。 でも要素の高さがウィンドウより大きいなら、下端が見切れないように、張り付く位置を上にずらしたい。 この調整をCSS変数でやります。:contentReference[oaicite:3]{index=3}

最小構成 まずは通常版を作る

HTML 重ねたいセクションに同じクラスを付ける

重ねたいセクションだけにクラスを付けます。 途中で演出を止めたい場所は relative にします。 この切り替えはマークアップでやった方が、JSがシンプルになります。:contentReference[oaicite:4]{index=4}

<section class="js-scroll-overlap">
  <div class="inner">
    <h2>Section A</h2>
    <p>重ねるセクション</p>
  </div>
</section>

<section class="js-scroll-overlap">
  <div class="inner">
    <h2>Section B</h2>
    <p>次のセクションが上に重なる</p>
  </div>
</section>

<section class="relative">
  <div class="inner">
    <h2>ここは重ねない</h2>
    <p>通常スクロール</p>
  </div>
</section>

<section class="js-scroll-overlap">
  <div class="inner">
    <h2>Section C</h2>
    <p>また重ねる</p>
  </div>
</section>

CSS stickyのtopをCSS変数で制御する

stickyのtopを –sticky-offset で制御します。 要素がウィンドウより小さい時は -1px にして、ほぼ top: 0 相当の見え方に。 要素が大きい時は、差分をマイナスで入れて、張り付く位置を上げます。:contentReference[oaicite:5]{index=5}

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

.relative {
  position: relative;
}

is-disabledは、キーボード操作時に動きを止めるために使います。 これを入れておくと、Tab移動でフォーカスが隠れる事故を減らせます。:contentReference[oaicite:6]{index=6}

JavaScript 要素の高さとウィンドウ高さでoffsetを計算する

ここが肝です。 やることはシンプルで、各セクションの高さを測って、ウィンドウ高さとの差分からオフセットを作る。 それをCSS変数に入れる。

function scrollOverlapBasic() {
  const targets = document.querySelectorAll('.js-scroll-overlap');
  if (!targets.length) return;

  let lastWinHeight = window.innerHeight;

  const setStickyOffset = () => {
    targets.forEach((target) => {
      const targetHeight = target.offsetHeight;
      const offsetValue =
        lastWinHeight > targetHeight
          ? '-1px'
          : `-${targetHeight - lastWinHeight}px`;
      target.style.setProperty('--sticky-offset', offsetValue);
    });
  };

  setStickyOffset();

  window.addEventListener('resize', () => {
    const winHeight = window.innerHeight;
    if (lastWinHeight !== winHeight) {
      lastWinHeight = winHeight;
      setStickyOffset();
    }
  });

  // bodyのサイズ変更も拾う(画像読み込みなど)
  const ro = new ResizeObserver(() => {
    setStickyOffset();
  });
  ro.observe(document.body);

  // キーボード操作時は無効化(フォーカスが隠れる事故対策)
  const toggleDisabled = (flag) => {
    targets.forEach((t) => t.classList.toggle('is-disabled', flag));
  };

  let isKeyboard = false;

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      isKeyboard = true;
      toggleDisabled(true);
    }
  });

  document.addEventListener('mousedown', () => {
    if (isKeyboard) {
      isKeyboard = false;
      toggleDisabled(false);
    }
  });
}

scrollOverlapBasic();

この実装は、元記事の考え方を踏襲しつつ、読みやすさを優先して整理した形です。:contentReference[oaicite:7]{index=7}

実務でハマるポイントを先に潰す

親や祖先にoverflow hiddenがあるとstickyが死ぬ

この演出が動かない時、まずCSSを見ます。 親コンテナやセクションラッパーに overflow: hidden が付いていないか。 特に、デザイン都合で「はみ出しを切る」指定が入っていると起きがちです。:contentReference[oaicite:8]{index=8}

対策は、stickyを使う要素の祖先に overflow を置かない設計にすること。 どうしても必要なら、切りたい要素だけ別ラッパーで包んで、sticky要素を外に出す。 現場ではだいたいこの2択です。

画像のレイアウトシフトで高さが狂う

セクション内に画像があると、読み込み前後で高さが変わります。 すると offsetHeight がズレて、stickyの位置もズレます。 元記事でも言及されているポイントです。:contentReference[oaicite:9]{index=9}

対策は二段構えが強いです。

  • imgにwidthとheightを入れて、読み込み前からレイアウトを確定させる
  • ResizeObserverでbodyの変化を拾って再計算する

これで実務の事故はかなり減ります。

フォーカスが隠れる問題は必ず意識する

重ねる演出は、見た目上は「手前にあるセクション」が常に上に来ます。 その状態でTab移動すると、フォーカスされたリンクやボタンが隠れることがあります。

なので、キーボード操作を検知したら is-disabled を付けて stickyを解除する。 この方針は実務でかなり効きます。:contentReference[oaicite:10]{index=10}

ここをサボると、レビューで突っ込まれるか、リリース後に気付いて青ざめます。 つまり、未来の自分が泣きます。

応用 オーバーレイ演出を入れて切り替えを強調する

やりたいことは暗幕をかけて切り替え感を出す

セクションが切り替わる時に、前のセクションが少し暗くなる。 これだけで「次に進んだ感」が出ます。 元記事ではdata属性でオンオフできる形が紹介されています。:contentReference[oaicite:11]{index=11}

HTML data-is-overlayで有効化する

<section class="js-scroll-overlap" data-is-overlay="true">...</section>
<section class="js-scroll-overlap" data-is-overlay="false">...</section>

CSS afterで暗幕を描き CSS変数で透明度を操作する

.js-scroll-overlap {
  position: relative;
}

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

.js-scroll-overlap[data-is-overlay="true"]:not(.is-disabled)::after {
  content: "";
  opacity: var(--overlay-opacity);
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

ポイントは pointer-events: none。 これを忘れると、暗幕がクリックを奪います。 そしてだいたい、公開直前に気付きます。

JS スクロール位置から透明度を線形に計算する

透明度は、開始位置から終了位置までの範囲を 0 から 1 に正規化して作ります。 元記事のロジックはこの考え方で組まれています。:contentReference[oaicite:12]{index=12}

実務で重要なのは、毎スクロールで全部の要素を計算しないこと。 IntersectionObserverで表示中の要素だけ更新する方針は、体感が軽くなります。:contentReference[oaicite:13]{index=13}

ここは長くなるので、設計だけ噛み砕いて言うとこうです。

  • 各セクションについて overlayStart と overlayEnd を事前計算してMapに持つ
  • スクロール位置 scrollTop を見て、範囲内なら opacity を算出
  • その値を –overlay-opacity にセット
  • 画面内にいる時だけイベントを付ける

もしこの応用版までやるなら、元記事のコードをベースにしつつ、プロジェクトの都合に合わせて関数を分割するのがおすすめです。:contentReference[oaicite:14]{index=14}

設計をもう一段強くする 実務向けの改善案

スクロールイベントは出来ればまとめて管理する

ページ内にスクロール演出が増えると、スクロールイベントが乱立します。 すると、どこが重いのか分からなくなる。

おすすめは、スクロールを監視する箇所を一つに寄せる設計です。 例えば、スクロール時はフラグだけ立て、requestAnimationFrameでまとめて処理する。 定番のやり方です。

prefers-reduced-motion reduce の時は演出を弱める

重なり演出やオーバーレイは、動きに弱い人には負担になる可能性があります。 reduceの時は、オーバーレイを無効にする、切り替えを短くするなど、配慮が出来ると品質が上がります。

@media (prefers-reduced-motion: reduce) {
  .js-scroll-overlap[data-is-overlay="true"]::after {
    opacity: 0 !important;
  }
}

この一行で、レビューの空気が良くなります。 現場の空気は、地味な配慮で守られます。

スマホのアドレスバー挙動に注意する

モバイルでは、アドレスバーの表示非表示で window.innerHeight が変わることがあります。 この実装はウィンドウ高さを使うので、リサイズ時の再計算は必須です。:contentReference[oaicite:15]{index=15}

記事のように「高さが変わった時だけ更新」する考え方は、無駄な計算を減らします。:contentReference[oaicite:16]{index=16}

WordPressで使う時の現実的なやり方

ブロックの増減があっても動くマークアップにする

WordPressは、後からブロックが増えたり減ったりします。 なので、重ねたいセクションだけにクラスを付ける運用が良いです。

  • 重ねる: js-scroll-overlap
  • 重ねない: relative(または何も付けない)

編集者が迷わないように、ブロックパターン化しておくと、さらに安定します。

画像は必ずサイズを持たせる

WordPressは画像が多いので、CLS対策が重要です。 widthとheight、もしくはaspect-ratioで高さを確定させてください。 元記事が言う「レイアウトシフトがあると高さ取得が狂う」は、その通りです。:contentReference[oaicite:17]{index=17}

デバッグ手順 動かない時に見る順番

1 stickyが効いているか

  • 対象要素が position: sticky になっているか
  • topが意図した値になっているか(CSS変数が入っているか)
  • 祖先要素に overflow: hidden などがないか :contentReference[oaicite:18]{index=18}

2 CSS変数 –sticky-offset が更新されているか

  • JSが動いているか(コンソールでエラーが出ていないか)
  • 対象要素のstyleに –sticky-offset が付与されているか

3 高さが正しく取れているか

  • 画像読み込み前後で高さが変わっていないか
  • ResizeObserverが発火して再計算されているか :contentReference[oaicite:19]{index=19}

4 Tab移動でおかしくなる場合

  • is-disabledが付くか
  • stickyが解除されるか
  • フォーカスが隠れなくなるか :contentReference[oaicite:20]{index=20}

この順で見れば、原因の特定は大体できます。

関連して知っておくと得をする話

stickyは万能ではないが味方にすると強い

stickyは、レイアウトの都合や親のoverflowで制約があります。 でも、その制約を理解して使うと、JSだけでやるより安定して軽いことが多いです。

今回みたいに「基本はCSSで実現して、足りない部分だけJSで補う」構成は、長期運用で効いてきます。

IntersectionObserverとResizeObserverは現代の現場装備

スクロール演出は、全部を常時計算すると重くなります。 表示中だけ処理する。 サイズが変わったら再計算する。 この二つを支えるのが IntersectionObserver と ResizeObserver です。 元記事でも採用されています。:contentReference[oaicite:21]{index=21}

特にWordPressやCMSでは、コンテンツが後から変わるので、ResizeObserverは実務でかなり頼れます。

演出は足し算より引き算が大事

重なり演出は、やり過ぎると逆に読みにくくなります。

  • セクション数が多いページでは、途中で relative を挟んで呼吸を作る
  • オーバーレイは全区間でなく、切り替わりの瞬間だけにする
  • 小さなカード一覧など情報量が多い場所では使わない

動きの設計は、情報設計でもあります。 この視点があると、実装の評価が一段上がります。

まとめ 速く作れて 事故りにくい重ねスクロールの作り方

スクロールでセクションが切り替わる時に次のコンテンツと重ねる表現は、 position: stickyの性質をうまく使うと、軽くて強い実装になります。:contentReference[oaicite:22]{index=22}

そして実務で勝つための要点は、次の4つです。

  • topは固定値にせず、要素の高さとウィンドウ高さでCSS変数を更新する
  • 画像などで高さが変わる前提で、ResizeObserverで再計算する
  • キーボード操作時は演出を止め、フォーカスが隠れないようにする
  • オーバーレイなどの演出は、表示中だけ計算して軽量化する

このセットで作ると、見た目はリッチで、中身は堅実。 つまり、現場で一番強いタイプの実装になります。

(Visited 5 times, 1 visits today)
タイトルとURLをコピーしました