「スクロールしていくと、次のセクションが前のセクションにぬるっと重なって切り替わる」。 この見せ方、LPやブランドサイトでよく見ます。派手すぎないのに雰囲気が出る。しかも、意外と離脱も減る。
ただし実装は、見た目よりも少しだけ気を使います。 雑に作ると、スマホでずれたり、画像が入った瞬間に高さが変わって破綻したり、Tabキーで移動したらフォーカスが隠れたり。 現場あるあるが詰まっています。
この記事では、下記の内容をベースにしつつ、実務で困りにくい形へ噛み砕いてまとめ直します。:contentReference[oaicite:0]{index=0}
- position: stickyを使って「重なり」を作る考え方
- 要素の高さとウィンドウ高さからオフセットを計算するコツ
- 画像やフォントでレイアウトがズレる問題の対策
- キーボード操作でのアクセシビリティ事故を避ける方法
- オーバーレイ演出(暗幕)を追加する応用
- WordPressでも崩れにくい設計とデバッグ手順
忙しい人向けに先に結論を言うと、勝ち筋はこうです。
- 重ねたい各セクションは「sticky」で画面に張り付かせる
- ただし「top」は固定値でなく、要素の高さに応じてCSS変数で動的に決める
- bodyのサイズ変化(画像読み込みなど)を監視して再計算する
- Tab移動の時は動きを止めて、フォーカスが隠れないようにする
この4つを押さえると、だいたいの現場で安定します。
この表現は何がうれしいのか
スクロール演出の中でもコスパが良い
フルスクリーンの派手なパララックスは、作るのも重いし、端末によって印象が変わりやすいです。 それに比べて「セクションを重ねて切り替える」は、動きの質感が出る割に実装が軽い。
しかも、コンテンツが普通に縦に並んでいる構造を大きく壊しにくいので、デザイン変更にも強いです。
ユーザーの体験を邪魔しにくい
スクロールを奪わない。勝手に横移動もしない。勝手にスナップもしない。 なので、ストレスを増やしにくいタイプの演出です。
ただし、アクセシビリティだけは油断すると一瞬で事故ります。 後半でしっかり潰します。
仕組みの核心 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で再計算する
- キーボード操作時は演出を止め、フォーカスが隠れないようにする
- オーバーレイなどの演出は、表示中だけ計算して軽量化する
このセットで作ると、見た目はリッチで、中身は堅実。 つまり、現場で一番強いタイプの実装になります。

