スクロール3D回転カード

スクロール量をrotateYに割り当て、カードを立体的に回転させます。CSS 3D transformの応用デモ。

#css#javascript#3d

ライブデモ

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

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

HTML
<!-- Sakura 新シングル特設。スクロールでCDジャケットが3D回転して裏面を見せる -->
<div class="skr-scroller" id="rcScroller">
  <div class="skr-spacer">
    <span class="skr-kicker">3rd SINGLE</span>
    <h1>はなびらメロディ</h1>
    <p>&#8595; スクロールでジャケットを回転</p>
  </div>

  <!-- スクロール量に応じて3D回転するCDジャケット -->
  <section class="skr-stage" id="rcStage">
    <div class="skr-card" id="rcCard">
      <!-- 表:ジャケット写真 -->
      <div class="skr-face skr-front">
        <span class="skr-photo"></span>
        <div class="skr-front-cap">
          <span class="skr-label">Sakura</span>
          <strong>はなびらメロディ</strong>
        </div>
      </div>
      <!-- 裏:収録曲リスト -->
      <div class="skr-face skr-back">
        <span class="skr-back-title">収録曲</span>
        <ol class="skr-tracks">
          <li>はなびらメロディ</li>
          <li>春風ステップ</li>
          <li>はなびらメロディ (Inst.)</li>
        </ol>
        <span class="skr-cat">SKR-0003 / 2026.4.1</span>
      </div>
    </div>
  </section>

  <div class="skr-spacer skr-end">&#127800; 4月リリース &#127800;</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

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

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
  background: radial-gradient(circle at 50% 0%, #fff0f6, #ffe1ec 70%);
  color: var(--ink);
}

/* 自前スクロール領域。3D回転に奥行きを与える */
.skr-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--pink-deep) transparent;
  perspective: 1000px;
}

.skr-spacer {
  height: 220px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  text-align: center;
}
.skr-kicker { font-size: .62rem; letter-spacing: .3em; color: var(--pink-deep); }
.skr-spacer h1 { font-size: 1.7rem; font-weight: 800; color: #c4396a; }
.skr-spacer p { font-size: .8rem; color: #9a7080; margin-top: 4px; }
.skr-end { font-size: 1rem; color: #c4396a; font-weight: 700; }

/* カードのステージ:高さでスクロール距離を確保 */
.skr-stage {
  height: 220vh;
  position: relative;
}
.skr-card {
  position: sticky;
  top: calc(50vh - 120px);
  width: 220px;
  height: 220px;
  margin: 0 auto;
  transform-style: preserve-3d;
  transform: rotateY(0deg);
  will-change: transform;
}
.skr-face {
  position: absolute;
  inset: 0;
  border-radius: 16px;
  backface-visibility: hidden;
  overflow: hidden;
  box-shadow: 0 30px 60px -22px rgba(196,57,106,.55);
}

/* 表:ジャケット写真 */
.skr-front {
  display: flex;
  align-items: flex-end;
  background: var(--pink);
}
.skr-photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-color: var(--pink); /* 読み込み前のフォールバック */
}
.skr-front::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(196,57,106,.75), transparent 55%);
}
.skr-front-cap {
  position: relative;
  z-index: 1;
  padding: 14px 16px;
  color: #fff;
}
.skr-label { display: block; font-size: .66rem; letter-spacing: .26em; opacity: .9; }
.skr-front-cap strong { font-size: 1.15rem; font-weight: 800; }

/* 裏:収録曲 */
.skr-back {
  transform: rotateY(180deg);
  background: linear-gradient(150deg, #fff, #fff2f7);
  border: 1px solid var(--pink);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  padding: 22px 20px;
}
.skr-back-title {
  font-size: .68rem;
  letter-spacing: .24em;
  color: var(--pink-deep);
  font-weight: 700;
}
.skr-tracks {
  list-style: none;
  counter-reset: t;
  display: grid;
  gap: 8px;
}
.skr-tracks li {
  counter-increment: t;
  font-size: .84rem;
  color: #6b5560;
  padding-left: 22px;
  position: relative;
}
.skr-tracks li::before {
  content: counter(t, decimal-leading-zero);
  position: absolute; left: 0;
  font-size: .68rem;
  font-weight: 700;
  color: var(--pink-deep);
}
.skr-cat { font-size: .66rem; color: #9a7080; letter-spacing: .08em; margin-top: 4px; }

@media (prefers-reduced-motion: reduce) {
  .skr-stage { height: auto; padding: 30px 0; }
  .skr-card { position: static; transform: none !important; }
}
JavaScript
// Sakura シングル:スクロール通過進捗(0〜1)を 0〜360度の回転に変換し、ジャケットを回す
(() => {
  const scroller = document.getElementById('rcScroller');
  const stage = document.getElementById('rcStage');
  const card = document.getElementById('rcCard');
  if (!scroller || !stage || !card) return; // null安全

  // ジャケット表面に picsum 写真を設定
  const photo = card.querySelector('.skr-photo');
  if (photo) photo.style.backgroundImage = 'url("https://picsum.photos/300/300?random=41")';

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では静止(CSS側)

  function render() {
    // stage の上端がスクロール領域上端を超えた量 ÷ 動かせる距離 = 進捗
    const passed = scroller.scrollTop - stage.offsetTop;
    const total = stage.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    const deg = progress * 360;
    // 軽く浮かせるためのscaleも併用
    const scale = 1 + Math.sin(progress * Math.PI) * 0.06;
    card.style.transform = `rotateY(${deg}deg) scale(${scale})`;
  }

  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 * 2.4;
    if (scroller.scrollTop >= max - 1) dir = -1;
    else if (scroller.scrollTop <= 1) dir = 1;
    requestAnimationFrame(step);
  }, 700);
})();

コード

HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="rc-scroller" id="rcScroller">
<div class="rc-spacer">&#8595; スクロールでカードが回転</div>

<!-- スクロール量に応じて3D回転するカード -->
<section class="rc-stage" id="rcStage">
  <div class="rc-card" id="rcCard">
    <div class="rc-face rc-front">
      <span class="rc-badge">3D</span>
      <h2>Scroll Rotate</h2>
      <p>スクロールでくるり</p>
    </div>
    <div class="rc-face rc-back">
      <span class="rc-badge">CSS</span>
      <h2>transform: rotateY</h2>
      <p>奥行きのある演出</p>
    </div>
  </div>
</section>

<div class="rc-spacer rc-end">&#10003;</div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 0%, #232a4d, #0c0e1a 70%);
  color: #fff;
}

/* プレビュー枠を埋める自前スクロール領域 */
.rc-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  /* 3D回転に奥行きを与える */
  perspective: 900px;
}

.rc-spacer {
  height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #8089b0;
  font-size: .9rem;
  letter-spacing: .08em;
}
.rc-end { color: #6ce0c0; }

/* カードのステージ:高さでスクロール距離を確保 */
.rc-stage {
  height: 200vh;
  position: relative;
}
.rc-card {
  position: sticky;
  top: calc(50vh - 130px);
  width: 220px;
  height: 260px;
  margin: 0 auto;
  transform-style: preserve-3d;
  transform: rotateY(0deg);
  will-change: transform;
}
.rc-face {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  box-shadow: 0 30px 60px rgba(0,0,0,.45);
}
.rc-front {
  background: linear-gradient(150deg, #6a5cff, #2f8fff);
}
.rc-back {
  background: linear-gradient(150deg, #ff5c8a, #ff9a3c);
  transform: rotateY(180deg);
}
.rc-badge {
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .2em;
  padding: 4px 10px;
  border: 1px solid rgba(255,255,255,.6);
  border-radius: 999px;
}
.rc-face h2 { font-size: 1.3rem; font-weight: 800; text-align: center; }
.rc-face p { font-size: .85rem; opacity: .92; }

@media (prefers-reduced-motion: reduce) {
  .rc-stage { height: auto; padding: 40px 0; }
  .rc-card { position: static; transform: none !important; }
}
JavaScript
// 自前スクロール領域の通過進捗(0〜1)を 0〜360度の回転に変換
(() => {
  const scroller = document.getElementById('rcScroller');
  const stage = document.getElementById('rcStage');
  const card = document.getElementById('rcCard');
  if (!scroller || !stage || !card) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では静止(CSS側)

  function render() {
    // stage の上端がスクロール領域上端を超えた量 ÷ 動かせる距離 = 進捗
    const passed = scroller.scrollTop - stage.offsetTop;
    const total = stage.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    const deg = progress * 360;
    // 軽く浮かせるためのscaleも併用
    const scale = 1 + Math.sin(progress * Math.PI) * 0.06;
    card.style.transform = `rotateY(${deg}deg) scale(${scale})`;
  }

  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 * 2.4;
    if (scroller.scrollTop >= max - 1) dir = -1;
    else if (scroller.scrollTop <= 1) dir = 1;
    requestAnimationFrame(step);
  }, 700);
})();

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

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

# 追加してほしい効果
スクロール3D回転カード(スクロール演出)
スクロール量をrotateYに割り当て、カードを立体的に回転させます。CSS 3D transformの応用デモ。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="rc-scroller" id="rcScroller">
<div class="rc-spacer">&#8595; スクロールでカードが回転</div>

<!-- スクロール量に応じて3D回転するカード -->
<section class="rc-stage" id="rcStage">
  <div class="rc-card" id="rcCard">
    <div class="rc-face rc-front">
      <span class="rc-badge">3D</span>
      <h2>Scroll Rotate</h2>
      <p>スクロールでくるり</p>
    </div>
    <div class="rc-face rc-back">
      <span class="rc-badge">CSS</span>
      <h2>transform: rotateY</h2>
      <p>奥行きのある演出</p>
    </div>
  </div>
</section>

<div class="rc-spacer rc-end">&#10003;</div>
</div>

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

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 0%, #232a4d, #0c0e1a 70%);
  color: #fff;
}

/* プレビュー枠を埋める自前スクロール領域 */
.rc-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  /* 3D回転に奥行きを与える */
  perspective: 900px;
}

.rc-spacer {
  height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #8089b0;
  font-size: .9rem;
  letter-spacing: .08em;
}
.rc-end { color: #6ce0c0; }

/* カードのステージ:高さでスクロール距離を確保 */
.rc-stage {
  height: 200vh;
  position: relative;
}
.rc-card {
  position: sticky;
  top: calc(50vh - 130px);
  width: 220px;
  height: 260px;
  margin: 0 auto;
  transform-style: preserve-3d;
  transform: rotateY(0deg);
  will-change: transform;
}
.rc-face {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  box-shadow: 0 30px 60px rgba(0,0,0,.45);
}
.rc-front {
  background: linear-gradient(150deg, #6a5cff, #2f8fff);
}
.rc-back {
  background: linear-gradient(150deg, #ff5c8a, #ff9a3c);
  transform: rotateY(180deg);
}
.rc-badge {
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .2em;
  padding: 4px 10px;
  border: 1px solid rgba(255,255,255,.6);
  border-radius: 999px;
}
.rc-face h2 { font-size: 1.3rem; font-weight: 800; text-align: center; }
.rc-face p { font-size: .85rem; opacity: .92; }

@media (prefers-reduced-motion: reduce) {
  .rc-stage { height: auto; padding: 40px 0; }
  .rc-card { position: static; transform: none !important; }
}

【JavaScript】
// 自前スクロール領域の通過進捗(0〜1)を 0〜360度の回転に変換
(() => {
  const scroller = document.getElementById('rcScroller');
  const stage = document.getElementById('rcStage');
  const card = document.getElementById('rcCard');
  if (!scroller || !stage || !card) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では静止(CSS側)

  function render() {
    // stage の上端がスクロール領域上端を超えた量 ÷ 動かせる距離 = 進捗
    const passed = scroller.scrollTop - stage.offsetTop;
    const total = stage.offsetHeight - scroller.clientHeight;
    const progress = total > 0
      ? Math.min(Math.max(passed / total, 0), 1)
      : 0;
    const deg = progress * 360;
    // 軽く浮かせるためのscaleも併用
    const scale = 1 + Math.sin(progress * Math.PI) * 0.06;
    card.style.transform = `rotateY(${deg}deg) scale(${scale})`;
  }

  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 * 2.4;
    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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。