スプリットスクリーン(左右逆スクロール)

内部スクロールに連動して左パネルは上へ、右パネルは下へと逆方向に動く分割画面。中央の境界線を境に視差が生まれ、対比のあるヒーローやストーリー導入に映えます。

#scroll#split#parallax

ライブデモ

使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。