スクロール3D回転カード
スクロール量をrotateYに割り当て、カードを立体的に回転させます。CSS 3D transformの応用デモ。
ライブデモ
使用例(お題: アイドルグループ 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>↓ スクロールでジャケットを回転</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">🌸 4月リリース 🌸</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">↓ スクロールでカードが回転</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">✓</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">↓ スクロールでカードが回転</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">✓</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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。