スプリットスクリーン(左右逆スクロール)
内部スクロールに連動して左パネルは上へ、右パネルは下へと逆方向に動く分割画面。中央の境界線を境に視差が生まれ、対比のあるヒーローやストーリー導入に映えます。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW こだわりページ。左右が逆方向に動く分割ヒーロー -->
<div class="mbs-scroller" id="splitScroller">
<div class="mbs-stage">
<!-- 左パネル:焙煎の世界。スクロールで上方向へ -->
<div class="mbs-pane mbs-left">
<span class="mbs-side-label">ROAST</span>
<div class="split-track" id="leftTrack">
<article class="mbs-card"><span class="mbs-num">01</span><h3>焙煎</h3></article>
<article class="mbs-card"><span class="mbs-num">02</span><h3>香り</h3></article>
<article class="mbs-card"><span class="mbs-num">03</span><h3>余韻</h3></article>
<article class="mbs-card"><span class="mbs-num">04</span><h3>一杯</h3></article>
</div>
</div>
<!-- 中央の境界線 -->
<div class="mbs-divider" aria-hidden="true"><span class="mbs-dot"></span></div>
<!-- 右パネル:豆の産地。スクロールで下方向へ(逆スクロール) -->
<div class="mbs-pane mbs-right">
<span class="mbs-side-label">ORIGIN</span>
<div class="split-track" id="rightTrack">
<article class="mbs-card"><span class="mbs-num">A</span><h3>農園</h3></article>
<article class="mbs-card"><span class="mbs-num">B</span><h3>標高</h3></article>
<article class="mbs-card"><span class="mbs-num">C</span><h3>収穫</h3></article>
<article class="mbs-card"><span class="mbs-num">D</span><h3>選別</h3></article>
</div>
</div>
</div>
<!-- スクロール量を稼ぐためのスペーサー -->
<div class="mbs-spacer" aria-hidden="true"></div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
--line: rgba(245,237,225,.4);
}
body {
font-family: "Hiragino Mincho ProN", "Yu Mincho", "Segoe UI", serif;
color: var(--cream);
background: var(--brown);
}
/* 自前スクロール領域 */
.mbs-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--amber) transparent;
}
/* sticky で枠に貼り付く分割ステージ */
.mbs-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
display: grid;
grid-template-columns: 1fr 2px 1fr;
overflow: hidden;
}
.mbs-pane {
position: relative;
overflow: hidden;
}
/* 左:深ブラウンの焙煎。右:琥珀の産地 */
.mbs-left { background: var(--brown); }
.mbs-right { background: linear-gradient(160deg, #b9772b, var(--amber)); }
.mbs-side-label {
position: absolute;
top: 14px; left: 14px;
z-index: 2;
font-family: "Segoe UI", sans-serif;
font-size: .58rem;
letter-spacing: .3em;
opacity: .7;
}
.mbs-right .mbs-side-label { left: auto; right: 14px; }
/* 縦に並ぶカード群。JSで translateY を上書き */
.split-track {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
will-change: transform;
}
.mbs-card {
text-align: center;
opacity: .95;
}
.mbs-num {
display: block;
font-family: "Segoe UI", sans-serif;
font-size: .72rem;
font-weight: 800;
letter-spacing: .3em;
opacity: .6;
margin-bottom: 6px;
}
.mbs-card h3 {
font-size: clamp(1.6rem, 6vw, 2.5rem);
font-weight: 700;
letter-spacing: .12em;
}
/* 中央の境界線 */
.mbs-divider {
position: relative;
background: var(--line);
}
.mbs-dot {
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--cream);
transform: translate(-50%, -50%);
box-shadow: 0 0 0 6px rgba(245,237,225,.14);
}
/* スクロール量を確保 */
.mbs-spacer { height: 220vh; }
@media (prefers-reduced-motion: reduce) {
.split-track { transition: none; }
}
JavaScript
// MOON BREW こだわり:スクロール進捗で左右パネルを逆方向に動かす
(() => {
const scroller = document.getElementById('splitScroller');
const left = document.getElementById('leftTrack');
const right = document.getElementById('rightTrack');
if (!scroller || !left || !right) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RANGE = 120; // ずらす最大量(px)
function render() {
const max = scroller.scrollHeight - scroller.clientHeight;
const p = max > 0 ? scroller.scrollTop / max : 0; // 進捗 0〜1
const shift = (p - 0.5) * 2 * RANGE; // 中央基準で -RANGE〜+RANGE
left.style.transform = `translate3d(0, ${-shift}px, 0)`; // 左は上へ
right.style.transform = `translate3d(0, ${shift}px, 0)`; // 右は下へ(逆方向)
}
let ticking = false;
scroller.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
}, { passive: true });
render(); // 初期位置
// 操作がなくても逆スクロールが伝わるよう、ゆっくり往復
let auto = !reduce;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 600);
}
})();
コード
HTML
<!-- 自前スクロール領域(プレビュー枠内で完結) -->
<div class="split-scroller" id="splitScroller">
<div class="split-stage">
<!-- 左パネル:スクロールで上方向へ移動 -->
<div class="split-pane split-left">
<div class="split-track" id="leftTrack">
<article class="split-card"><span class="split-num">01</span><h3>静寂</h3></article>
<article class="split-card"><span class="split-num">02</span><h3>余白</h3></article>
<article class="split-card"><span class="split-num">03</span><h3>陰影</h3></article>
<article class="split-card"><span class="split-num">04</span><h3>調和</h3></article>
</div>
</div>
<!-- 中央の境界線 -->
<div class="split-divider" aria-hidden="true"><span class="split-dot"></span></div>
<!-- 右パネル:スクロールで下方向へ移動(逆スクロール) -->
<div class="split-pane split-right">
<div class="split-track" id="rightTrack">
<article class="split-card"><span class="split-num">A</span><h3>躍動</h3></article>
<article class="split-card"><span class="split-num">B</span><h3>衝動</h3></article>
<article class="split-card"><span class="split-num">C</span><h3>飛翔</h3></article>
<article class="split-card"><span class="split-num">D</span><h3>爆発</h3></article>
</div>
</div>
</div>
<!-- スクロール量を稼ぐためのスペーサー -->
<div class="split-spacer" aria-hidden="true"></div>
</div>
CSS
:root {
--left-bg: #1c2333; /* 左パネル:静の色 */
--right-bg: #b8324a; /* 右パネル:動の色 */
--line: rgba(255, 255, 255, .35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
color: #fff;
background: #0d0f17;
}
/* プレビュー枠を埋める自前スクロール領域 */
.split-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
/* sticky で枠に貼り付く分割ステージ */
.split-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
display: grid;
grid-template-columns: 1fr 2px 1fr;
overflow: hidden;
}
.split-pane {
position: relative;
overflow: hidden;
}
.split-left { background: var(--left-bg); }
.split-right { background: var(--right-bg); }
/* 縦に並ぶカード群。JSで translateY を上書き */
.split-track {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
will-change: transform;
}
.split-card {
text-align: center;
opacity: .92;
}
.split-num {
display: block;
font-size: .8rem;
font-weight: 800;
letter-spacing: .3em;
opacity: .65;
margin-bottom: 6px;
}
.split-card h3 {
font-size: clamp(1.6rem, 6vw, 2.6rem);
font-weight: 800;
letter-spacing: .04em;
}
/* 中央の境界線 */
.split-divider {
position: relative;
background: var(--line);
}
.split-dot {
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
transform: translate(-50%, -50%);
box-shadow: 0 0 0 6px rgba(255, 255, 255, .12);
}
/* スクロール量を確保 */
.split-spacer { height: 220vh; }
@media (prefers-reduced-motion: reduce) {
.split-track { transition: none; }
}
JavaScript
// 自前スクロール領域の進捗で、左右パネルを逆方向に動かす
(() => {
const scroller = document.getElementById('splitScroller');
const left = document.getElementById('leftTrack');
const right = document.getElementById('rightTrack');
if (!scroller || !left || !right) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RANGE = 120; // ずらす最大量(px)
function render() {
const max = scroller.scrollHeight - scroller.clientHeight;
const p = max > 0 ? scroller.scrollTop / max : 0; // 進捗 0〜1
const shift = (p - 0.5) * 2 * RANGE; // 中央基準で -RANGE〜+RANGE
left.style.transform = `translate3d(0, ${-shift}px, 0)`; // 左は上へ
right.style.transform = `translate3d(0, ${shift}px, 0)`; // 右は下へ(逆方向)
}
let ticking = false;
scroller.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
}, { passive: true });
render(); // 初期位置
// 操作がなくても逆スクロールが伝わるよう、ゆっくり往復
let auto = !reduce;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 600);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スプリットスクリーン(左右逆スクロール)」の効果を追加してください。
# 追加してほしい効果
スプリットスクリーン(左右逆スクロール)(スクロール演出)
内部スクロールに連動して左パネルは上へ、右パネルは下へと逆方向に動く分割画面。中央の境界線を境に視差が生まれ、対比のあるヒーローやストーリー導入に映えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前スクロール領域(プレビュー枠内で完結) -->
<div class="split-scroller" id="splitScroller">
<div class="split-stage">
<!-- 左パネル:スクロールで上方向へ移動 -->
<div class="split-pane split-left">
<div class="split-track" id="leftTrack">
<article class="split-card"><span class="split-num">01</span><h3>静寂</h3></article>
<article class="split-card"><span class="split-num">02</span><h3>余白</h3></article>
<article class="split-card"><span class="split-num">03</span><h3>陰影</h3></article>
<article class="split-card"><span class="split-num">04</span><h3>調和</h3></article>
</div>
</div>
<!-- 中央の境界線 -->
<div class="split-divider" aria-hidden="true"><span class="split-dot"></span></div>
<!-- 右パネル:スクロールで下方向へ移動(逆スクロール) -->
<div class="split-pane split-right">
<div class="split-track" id="rightTrack">
<article class="split-card"><span class="split-num">A</span><h3>躍動</h3></article>
<article class="split-card"><span class="split-num">B</span><h3>衝動</h3></article>
<article class="split-card"><span class="split-num">C</span><h3>飛翔</h3></article>
<article class="split-card"><span class="split-num">D</span><h3>爆発</h3></article>
</div>
</div>
</div>
<!-- スクロール量を稼ぐためのスペーサー -->
<div class="split-spacer" aria-hidden="true"></div>
</div>
【CSS】
:root {
--left-bg: #1c2333; /* 左パネル:静の色 */
--right-bg: #b8324a; /* 右パネル:動の色 */
--line: rgba(255, 255, 255, .35);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
color: #fff;
background: #0d0f17;
}
/* プレビュー枠を埋める自前スクロール領域 */
.split-scroller {
width: 100%;
height: 100vh;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
}
/* sticky で枠に貼り付く分割ステージ */
.split-stage {
position: sticky;
top: 0;
height: 100vh;
max-height: 100%;
display: grid;
grid-template-columns: 1fr 2px 1fr;
overflow: hidden;
}
.split-pane {
position: relative;
overflow: hidden;
}
.split-left { background: var(--left-bg); }
.split-right { background: var(--right-bg); }
/* 縦に並ぶカード群。JSで translateY を上書き */
.split-track {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
will-change: transform;
}
.split-card {
text-align: center;
opacity: .92;
}
.split-num {
display: block;
font-size: .8rem;
font-weight: 800;
letter-spacing: .3em;
opacity: .65;
margin-bottom: 6px;
}
.split-card h3 {
font-size: clamp(1.6rem, 6vw, 2.6rem);
font-weight: 800;
letter-spacing: .04em;
}
/* 中央の境界線 */
.split-divider {
position: relative;
background: var(--line);
}
.split-dot {
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
transform: translate(-50%, -50%);
box-shadow: 0 0 0 6px rgba(255, 255, 255, .12);
}
/* スクロール量を確保 */
.split-spacer { height: 220vh; }
@media (prefers-reduced-motion: reduce) {
.split-track { transition: none; }
}
【JavaScript】
// 自前スクロール領域の進捗で、左右パネルを逆方向に動かす
(() => {
const scroller = document.getElementById('splitScroller');
const left = document.getElementById('leftTrack');
const right = document.getElementById('rightTrack');
if (!scroller || !left || !right) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RANGE = 120; // ずらす最大量(px)
function render() {
const max = scroller.scrollHeight - scroller.clientHeight;
const p = max > 0 ? scroller.scrollTop / max : 0; // 進捗 0〜1
const shift = (p - 0.5) * 2 * RANGE; // 中央基準で -RANGE〜+RANGE
left.style.transform = `translate3d(0, ${-shift}px, 0)`; // 左は上へ
right.style.transform = `translate3d(0, ${shift}px, 0)`; // 右は下へ(逆方向)
}
let ticking = false;
scroller.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { render(); ticking = false; });
}, { passive: true });
render(); // 初期位置
// 操作がなくても逆スクロールが伝わるよう、ゆっくり往復
let auto = !reduce;
let dir = 1;
const stop = () => { auto = false; };
['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
scroller.addEventListener(ev, stop, { passive: true }));
if (auto) {
setTimeout(function step() {
if (!auto) return;
const max = scroller.scrollHeight - scroller.clientHeight;
if (max <= 0) return;
scroller.scrollTop += dir * 1.6;
if (scroller.scrollTop >= max - 1) dir = -1;
else if (scroller.scrollTop <= 1) dir = 1;
requestAnimationFrame(step);
}, 600);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。