横スクロールセクション

縦スクロール量をtransformで横移動に変換するピン留めセクション。ポートフォリオや工程紹介に映えます。

#javascript#sticky#animation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura メンバー紹介。縦スクロールが横移動になり、メンバーカードが流れる -->
<div class="skh-scroller" id="hsScroller">
  <div class="skh-intro">
    <span class="skh-logo">Sakura</span>
    <h1>MEMBER</h1>
    <p>&#8595; スクロールでメンバーを横に紹介</p>
  </div>

  <!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
  <section class="skh-pin" id="hsPin">
    <div class="skh-track" id="hsTrack">
      <article class="skh-card" style="--seed:31;--accent:#ff8fb3;">
        <span class="skh-no">01</span>
        <span class="skh-photo"></span>
        <h2>ひなた</h2>
        <p class="skh-role">センター / リーダー</p>
        <p class="skh-msg">いつも笑顔をいちばん前で。</p>
      </article>
      <article class="skh-card" style="--seed:32;--accent:#ffa6c4;">
        <span class="skh-no">02</span>
        <span class="skh-photo"></span>
        <h2>みお</h2>
        <p class="skh-role">メインボーカル</p>
        <p class="skh-msg">歌で気持ちを届けたい。</p>
      </article>
      <article class="skh-card" style="--seed:33;--accent:#ffb6cd;">
        <span class="skh-no">03</span>
        <span class="skh-photo"></span>
        <h2>こはる</h2>
        <p class="skh-role">ダンスリーダー</p>
        <p class="skh-msg">キレッキレでいくよ!</p>
      </article>
      <article class="skh-card" style="--seed:34;--accent:#ff9fbd;">
        <span class="skh-no">04</span>
        <span class="skh-photo"></span>
        <h2>あおい</h2>
        <p class="skh-role">作詞担当</p>
        <p class="skh-msg">言葉に想いをのせて。</p>
      </article>
      <article class="skh-card" style="--seed:35;--accent:#ffc2d6;">
        <span class="skh-no">05</span>
        <span class="skh-photo"></span>
        <h2>ゆい</h2>
        <p class="skh-role">最年少 / ムードメーカー</p>
        <p class="skh-msg">みんなを元気にしちゃう!</p>
      </article>
    </div>
  </section>

  <div class="skh-outro">
    <h2>5人で、ひとつの春を。</h2>
    <p>個性あふれるメンバーが集まって、Sakura はできています。ライブ会場で会えるのを楽しみにしています。</p>
  </div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --pink: #ffd1e0;
  --pink-deep: #ff8fb3;
  --gray: #eef0f3;
  --ink: #5a4853;
}

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
  background: #fff5f9;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* 自前スクロール領域 */
.skh-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--pink-deep) transparent;
}

.skh-intro, .skh-outro {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 24px;
}
.skh-intro {
  height: 220px;
  gap: 4px;
  background: linear-gradient(180deg, #fff6fa, var(--pink));
}
.skh-logo { font-size: 1rem; letter-spacing: .2em; color: var(--pink-deep); font-weight: 700; }
.skh-intro h1 { font-size: 2rem; font-weight: 800; letter-spacing: .14em; color: #c4396a; }
.skh-intro p { font-size: .8rem; color: #9a7080; margin-top: 4px; }

.skh-outro {
  min-height: 220px;
  gap: 12px;
  padding: 50px 24px 80px;
  background: #fff;
}
.skh-outro h2 { font-size: 1.4rem; font-weight: 800; color: #c4396a; }
.skh-outro p { max-width: 460px; color: #7a6470; line-height: 1.85; font-size: .9rem; }

/* スクロール距離を稼ぐ高い領域 */
.skh-pin {
  position: relative;
  height: 360vh; /* この高さの間だけ横移動 */
  background: linear-gradient(180deg, var(--pink) 0%, #fff5f9 12%);
}
.skh-track {
  position: sticky;
  top: 0;
  height: 100vh;
  min-height: 360px;
  display: flex;
  align-items: center;
  will-change: transform;
}

.skh-card {
  position: relative;
  flex: 0 0 70%;
  height: 78%;
  margin: 0 10px;
  padding: 22px 18px;
  border-radius: 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  background: #fff;
  border: 1px solid var(--pink);
  box-shadow: 0 22px 44px -24px rgba(255,143,179,.7);
}
.skh-card:first-child { margin-left: 24px; }
.skh-card:last-child { margin-right: 24px; }
.skh-no {
  font-size: .72rem;
  letter-spacing: .3em;
  color: var(--accent);
  font-weight: 800;
}
.skh-photo {
  width: 110px; height: 110px;
  border-radius: 50%;
  background-size: cover;
  background-position: center;
  background-color: var(--pink); /* 読み込み前のフォールバック */
  border: 3px solid var(--accent);
  margin: 4px 0;
}
.skh-card h2 { font-size: 1.5rem; font-weight: 800; color: #c4396a; }
.skh-role {
  font-size: .72rem;
  letter-spacing: .06em;
  color: #fff;
  background: var(--accent);
  padding: 3px 12px;
  border-radius: 999px;
}
.skh-msg { font-size: .84rem; color: #7a6470; margin-top: 4px; }

/* 動きを減らす設定:横移動をやめ縦並びに */
@media (prefers-reduced-motion: reduce) {
  .skh-pin { height: auto; }
  .skh-track {
    position: static;
    height: auto;
    flex-direction: column;
    transform: none !important;
  }
  .skh-card { flex-basis: auto; width: 86%; height: auto; margin: 12px auto; }
}
JavaScript
// Sakura メンバー紹介:縦スクロール量を、メンバートラックの横移動へ変換
(() => {
  const scroller = document.getElementById('hsScroller');
  const pin = document.getElementById('hsPin');
  const track = document.getElementById('hsTrack');
  if (!scroller || !pin || !track) return; // null安全

  // 各メンバー写真に picsum を設定(--seedを利用)
  Array.from(track.querySelectorAll('.skh-photo')).forEach(photo => {
    const card = photo.closest('.skh-card');
    const seed = card ? getComputedStyle(card).getPropertyValue('--seed').trim() : '';
    photo.style.backgroundImage = `url("https://picsum.photos/160/160?random=${seed || '1'}")`;
  });

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる

  // 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
  function render() {
    const passed = scroller.scrollTop - pin.offsetTop;
    const total = pin.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    // トラックがはみ出す幅だけ横移動
    const maxShift = track.scrollWidth - track.clientWidth;
    track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
  }

  let ticking = false;
  scroller.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { render(); ticking = false; });
  }, { passive: true });
  window.addEventListener('resize', render);
  render(); // 初期化

  // 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
  let auto = true;
  let dir = 1;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  setTimeout(function step() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (max <= 0) return;
    scroller.scrollTop += dir * 3;
    if (scroller.scrollTop >= max - 1) dir = -1;
    else if (scroller.scrollTop <= 1) dir = 1;
    requestAnimationFrame(step);
  }, 700);
})();

コード

HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="hs-scroller" id="hsScroller">
<div class="hs-intro">
  <span>&#8595; 縦スクロールが横移動に変わる</span>
</div>

<!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
<section class="hs-pin" id="hsPin">
  <div class="hs-track" id="hsTrack">
    <article class="hs-panel" style="--c1:#ff6a3d;--c2:#f9484a;">
      <span class="hs-idx">01</span><h2>Discover</h2>
      <p>横に流れるパネル</p>
    </article>
    <article class="hs-panel" style="--c1:#7a5cff;--c2:#4a2fd6;">
      <span class="hs-idx">02</span><h2>Design</h2>
      <p>縦スクロール量に連動</p>
    </article>
    <article class="hs-panel" style="--c1:#11b3a3;--c2:#0a8f7e;">
      <span class="hs-idx">03</span><h2>Develop</h2>
      <p>transformで横移動</p>
    </article>
    <article class="hs-panel" style="--c1:#f7b500;--c2:#f78f00;">
      <span class="hs-idx">04</span><h2>Deliver</h2>
      <p>ポートフォリオに最適</p>
    </article>
  </div>
</section>

<div class="hs-outro">
  <h2>縦→横の変換テクニック</h2>
  <p>固定領域の高さでスクロール距離を確保し、その進捗をパネルの横移動に割り当てています。</p>
</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #101018;
  color: #fff;
}

/* プレビュー枠を埋める自前スクロール領域 */
.hs-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.hs-intro, .hs-outro {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 24px;
}
.hs-intro {
  height: 220px;
  color: #9aa0b5;
  font-size: .9rem;
  letter-spacing: .06em;
}
.hs-outro {
  min-height: 240px;
  gap: 12px;
  padding: 56px 24px 90px;
}
.hs-outro h2 { font-size: 1.5rem; }
.hs-outro p { max-width: 480px; color: #b6bbcd; line-height: 1.7; }

/* スクロール距離を稼ぐための高い領域 */
.hs-pin {
  position: relative;
  height: 320vh; /* この高さの間だけ横移動 */
}
.hs-track {
  position: sticky;
  top: 0;
  height: 100vh;
  min-height: 360px;
  display: flex;
  align-items: center;
  will-change: transform;
}

.hs-panel {
  position: relative;
  flex: 0 0 78%;
  height: 76%;
  margin: 0 11px;
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: linear-gradient(140deg, var(--c1), var(--c2));
  box-shadow: 0 20px 50px rgba(0,0,0,.35);
}
.hs-panel:first-child { margin-left: 24px; }
.hs-panel:last-child { margin-right: 24px; }
.hs-idx {
  font-size: .85rem;
  letter-spacing: .4em;
  opacity: .85;
}
.hs-panel h2 {
  font-size: 2.2rem;
  font-weight: 800;
  text-shadow: 0 4px 18px rgba(0,0,0,.25);
}
.hs-panel p { font-size: .9rem; opacity: .92; }

/* 動きを減らす設定:横移動をやめ縦並び風に */
@media (prefers-reduced-motion: reduce) {
  .hs-pin { height: auto; }
  .hs-track {
    position: static;
    height: auto;
    flex-direction: column;
    transform: none !important;
  }
  .hs-panel { flex-basis: auto; width: 90%; height: 220px; margin: 12px auto; }
}
JavaScript
// 自前スクロール領域の縦スクロール量を、トラックの横移動へ変換
(() => {
  const scroller = document.getElementById('hsScroller');
  const pin = document.getElementById('hsPin');
  const track = document.getElementById('hsTrack');
  if (!scroller || !pin || !track) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる

  // 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
  function render() {
    // pin の上端がスクロール領域の上端を超えた量 ÷ 動かせる距離 = 進捗
    const passed = scroller.scrollTop - pin.offsetTop;
    const total = pin.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    // トラックがはみ出す幅だけ横移動
    const maxShift = track.scrollWidth - track.clientWidth;
    track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
  }

  let ticking = false;
  const onScroll = () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { render(); ticking = false; });
  };
  scroller.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', render);
  render(); // 初期化

  // 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
  let auto = true;
  let dir = 1;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  setTimeout(function step() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (max <= 0) return;
    scroller.scrollTop += dir * 3;
    if (scroller.scrollTop >= max - 1) dir = -1;
    else if (scroller.scrollTop <= 1) dir = 1;
    requestAnimationFrame(step);
  }, 700);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「横スクロールセクション」の効果を追加してください。

# 追加してほしい効果
横スクロールセクション(スクロール演出)
縦スクロール量をtransformで横移動に変換するピン留めセクション。ポートフォリオや工程紹介に映えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="hs-scroller" id="hsScroller">
<div class="hs-intro">
  <span>&#8595; 縦スクロールが横移動に変わる</span>
</div>

<!-- stickyで固定された高い領域。縦スクロール量を横移動に変換 -->
<section class="hs-pin" id="hsPin">
  <div class="hs-track" id="hsTrack">
    <article class="hs-panel" style="--c1:#ff6a3d;--c2:#f9484a;">
      <span class="hs-idx">01</span><h2>Discover</h2>
      <p>横に流れるパネル</p>
    </article>
    <article class="hs-panel" style="--c1:#7a5cff;--c2:#4a2fd6;">
      <span class="hs-idx">02</span><h2>Design</h2>
      <p>縦スクロール量に連動</p>
    </article>
    <article class="hs-panel" style="--c1:#11b3a3;--c2:#0a8f7e;">
      <span class="hs-idx">03</span><h2>Develop</h2>
      <p>transformで横移動</p>
    </article>
    <article class="hs-panel" style="--c1:#f7b500;--c2:#f78f00;">
      <span class="hs-idx">04</span><h2>Deliver</h2>
      <p>ポートフォリオに最適</p>
    </article>
  </div>
</section>

<div class="hs-outro">
  <h2>縦→横の変換テクニック</h2>
  <p>固定領域の高さでスクロール距離を確保し、その進捗をパネルの横移動に割り当てています。</p>
</div>
</div>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #101018;
  color: #fff;
}

/* プレビュー枠を埋める自前スクロール領域 */
.hs-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.hs-intro, .hs-outro {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 24px;
}
.hs-intro {
  height: 220px;
  color: #9aa0b5;
  font-size: .9rem;
  letter-spacing: .06em;
}
.hs-outro {
  min-height: 240px;
  gap: 12px;
  padding: 56px 24px 90px;
}
.hs-outro h2 { font-size: 1.5rem; }
.hs-outro p { max-width: 480px; color: #b6bbcd; line-height: 1.7; }

/* スクロール距離を稼ぐための高い領域 */
.hs-pin {
  position: relative;
  height: 320vh; /* この高さの間だけ横移動 */
}
.hs-track {
  position: sticky;
  top: 0;
  height: 100vh;
  min-height: 360px;
  display: flex;
  align-items: center;
  will-change: transform;
}

.hs-panel {
  position: relative;
  flex: 0 0 78%;
  height: 76%;
  margin: 0 11px;
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: linear-gradient(140deg, var(--c1), var(--c2));
  box-shadow: 0 20px 50px rgba(0,0,0,.35);
}
.hs-panel:first-child { margin-left: 24px; }
.hs-panel:last-child { margin-right: 24px; }
.hs-idx {
  font-size: .85rem;
  letter-spacing: .4em;
  opacity: .85;
}
.hs-panel h2 {
  font-size: 2.2rem;
  font-weight: 800;
  text-shadow: 0 4px 18px rgba(0,0,0,.25);
}
.hs-panel p { font-size: .9rem; opacity: .92; }

/* 動きを減らす設定:横移動をやめ縦並び風に */
@media (prefers-reduced-motion: reduce) {
  .hs-pin { height: auto; }
  .hs-track {
    position: static;
    height: auto;
    flex-direction: column;
    transform: none !important;
  }
  .hs-panel { flex-basis: auto; width: 90%; height: 220px; margin: 12px auto; }
}

【JavaScript】
// 自前スクロール領域の縦スクロール量を、トラックの横移動へ変換
(() => {
  const scroller = document.getElementById('hsScroller');
  const pin = document.getElementById('hsPin');
  const track = document.getElementById('hsTrack');
  if (!scroller || !pin || !track) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では縦並び(CSS側)に任せる

  // 固定領域の進捗(0〜1)を計算し、トラックを横へ動かす
  function render() {
    // pin の上端がスクロール領域の上端を超えた量 ÷ 動かせる距離 = 進捗
    const passed = scroller.scrollTop - pin.offsetTop;
    const total = pin.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    // トラックがはみ出す幅だけ横移動
    const maxShift = track.scrollWidth - track.clientWidth;
    track.style.transform = `translate3d(${-progress * maxShift}px,0,0)`;
  }

  let ticking = false;
  const onScroll = () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { render(); ticking = false; });
  };
  scroller.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', render);
  render(); // 初期化

  // 操作がなくても横移動が見えるよう、ゆっくり往復スクロール
  let auto = true;
  let dir = 1;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  setTimeout(function step() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (max <= 0) return;
    scroller.scrollTop += dir * 3;
    if (scroller.scrollTop >= max - 1) dir = -1;
    else if (scroller.scrollTop <= 1) dir = 1;
    requestAnimationFrame(step);
  }, 700);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。