マウス連動パララックス層
複数レイヤーをマウス位置に応じて異なる量だけ動かし、奥行きを演出します。ヒーローセクションの没入感を高める手法です。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW:マウス連動パララックスのカフェ・ヒーロー -->
<section class="mb-hero" id="mbHero" aria-label="MOON BREW ヒーロー">
<!-- 奥から手前へ:背景→豆→湯気→コップ→コピー の各層が異なる量で動く -->
<div class="mb-layer mb-layer--bg" data-depth="8" aria-hidden="true"></div>
<div class="mb-layer mb-layer--beans" data-depth="18" aria-hidden="true">
<span class="mb-bean" style="left:12%;top:24%"></span>
<span class="mb-bean" style="left:78%;top:18%"></span>
<span class="mb-bean" style="left:64%;top:70%"></span>
<span class="mb-bean" style="left:22%;top:74%"></span>
</div>
<div class="mb-layer mb-layer--steam" data-depth="30" aria-hidden="true">
<span class="mb-steam"></span><span class="mb-steam"></span><span class="mb-steam"></span>
</div>
<div class="mb-layer mb-layer--cup" data-depth="42" aria-hidden="true">
<span class="mb-cup">☕</span>
</div>
<div class="mb-layer mb-layer--copy" data-depth="14">
<span class="mb-kicker">SINCE 2014 / 自家焙煎</span>
<h1 class="mb-title">月のように<br>静かな一杯を。</h1>
<p class="mb-lead">深煎りの香りと、ゆるやかな時間。<br>あなたの夜にそっと寄り添うカフェ。</p>
<button class="mb-btn" type="button">本日のメニューを見る</button>
</div>
<p class="mb-tip">マウスを動かすと奥行きが生まれます</p>
</section>
CSS
/* MOON BREW:パララックス・カフェヒーロー */
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Mincho ProN", "Georgia", "Segoe UI", serif;
background: var(--brown);
color: var(--cream);
overflow: hidden;
}
.mb-hero {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
background: radial-gradient(circle at 70% 30%, #3a2817 0%, var(--brown) 70%);
}
/* 各層は中央基準で transform:JSがマウス量を反映 */
.mb-layer {
position: absolute;
inset: -6%;
transition: transform 0.25s ease-out;
will-change: transform;
}
/* 背景:木目調グラデの写真 */
.mb-layer--bg {
background: url("https://picsum.photos/640/440?random=51") center/cover no-repeat;
filter: brightness(.42) sepia(.4) saturate(1.3);
}
.mb-layer--bg::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(120deg, rgba(43,29,18,.7), rgba(43,29,18,.25));
}
/* 浮遊するコーヒー豆 */
.mb-bean {
position: absolute;
width: 22px; height: 15px;
background: radial-gradient(circle at 40% 35%, #6b4a2a, #2e1d10);
border-radius: 50% / 60%;
box-shadow: 0 4px 10px rgba(0,0,0,.4);
}
.mb-bean::after {
content: ""; position: absolute; inset: 0;
border-left: 1.5px solid rgba(20,12,6,.8);
transform: rotate(20deg);
margin: 0 auto; width: 0;
}
/* 湯気 */
.mb-layer--steam { display: grid; place-items: center; }
.mb-steam {
position: absolute;
bottom: 46%;
width: 8px; height: 60px;
background: linear-gradient(to top, rgba(255,255,255,.0), rgba(255,255,255,.28));
filter: blur(4px);
border-radius: 50%;
animation: mbRise 4s ease-in-out infinite;
}
.mb-steam:nth-child(2) { transform: translateX(-18px); animation-delay: -1.3s; }
.mb-steam:nth-child(3) { transform: translateX(18px); animation-delay: -2.6s; }
@keyframes mbRise {
0% { opacity: 0; transform: translateY(10px) scaleY(.8); }
40% { opacity: 1; }
100% { opacity: 0; transform: translateY(-30px) scaleY(1.2); }
}
/* カップ */
.mb-layer--cup { display: grid; place-items: center; }
.mb-cup {
font-size: 92px;
margin-top: 40px;
filter: drop-shadow(0 14px 18px rgba(0,0,0,.5));
}
/* コピー層 */
.mb-layer--copy {
inset: auto;
left: 32px; top: 60px;
transition: transform 0.18s ease-out;
}
.mb-kicker {
font-size: 11px; letter-spacing: 0.22em; color: var(--amber); font-weight: 700;
}
.mb-title {
margin: 10px 0; font-size: 32px; line-height: 1.25; font-weight: 800;
text-shadow: 0 4px 16px rgba(0,0,0,.55);
}
.mb-lead { margin: 0 0 18px; font-size: 13px; line-height: 1.8; color: rgba(245,237,225,.85); }
.mb-btn {
font: inherit; font-size: 13px; font-weight: 700;
color: var(--brown);
background: linear-gradient(135deg, #e0a85a, var(--amber));
border: none; padding: 11px 22px; border-radius: 999px; cursor: pointer;
box-shadow: 0 8px 18px rgba(201,138,59,.4);
transition: transform 0.2s ease;
}
.mb-btn:hover { transform: translateY(-2px); }
.mb-tip {
position: absolute; bottom: 12px; right: 18px; margin: 0;
font-size: 11px; color: rgba(245,237,225,.5);
}
@media (prefers-reduced-motion: reduce) {
.mb-steam { animation: none; opacity: .6; }
.mb-layer { transition: none; }
}
JavaScript
// MOON BREW パララックス:マウス位置で各層を depth 量だけ動かし奥行きを演出
(() => {
const hero = document.getElementById("mbHero");
if (!hero) return; // null安全
const layers = Array.from(hero.querySelectorAll(".mb-layer"));
if (!layers.length) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// -0.5〜0.5 の正規化座標から各層を移動
const move = (nx, ny) => {
for (const el of layers) {
const d = Number(el.dataset.depth) || 0;
el.style.transform = `translate3d(${-nx * d}px, ${-ny * d}px, 0)`;
}
};
hero.addEventListener("pointermove", (e) => {
if (reduce) return;
const r = hero.getBoundingClientRect();
const nx = (e.clientX - r.left) / r.width - 0.5;
const ny = (e.clientY - r.top) / r.height - 0.5;
move(nx, ny);
});
// 離れたら中央へ戻す
hero.addEventListener("pointerleave", () => move(0, 0));
// ボタンの軽いフィードバック
const btn = hero.querySelector(".mb-btn");
btn?.addEventListener("click", () => {
const t = btn.textContent;
btn.textContent = "本日のおすすめは深煎りです ☕";
setTimeout(() => { btn.textContent = t; }, 1600);
});
})();
コード
HTML
<div class="parallax" id="parallax" aria-label="マウス連動パララックスのデモ">
<!-- data-depth が大きいほど大きく動く -->
<span class="px-layer px-stars" data-depth="0.15"></span>
<span class="px-layer px-glow" data-depth="0.3"></span>
<span class="px-blob px-blob--a" data-depth="0.6"></span>
<span class="px-blob px-blob--b" data-depth="0.9"></span>
<div class="px-content" data-depth="1.4">
<span class="px-kicker">PARALLAX</span>
<h2 class="px-title">奥行きを、<br>マウスで感じる</h2>
<p class="px-sub">カーソルを動かしてみてください</p>
</div>
<span class="px-ring" data-depth="2.2"></span>
</div>
CSS
/* ===== マウス連動パララックス層 ===== */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
font-family: "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
.parallax {
position: relative;
width: 100%;
height: 360px;
display: grid;
place-items: center;
background: radial-gradient(circle at 50% 30%, #1d2a55 0%, #0a0f24 55%, #05060f 100%);
cursor: crosshair;
}
.px-layer, .px-blob, .px-ring {
position: absolute;
pointer-events: none;
will-change: transform;
}
/* glow と ring は固定サイズなので inset:0 + margin:auto で親中央に配置。
JS が transform(translate3d) を上書きしても中央基準は崩れない */
.px-glow, .px-ring {
inset: 0;
margin: auto;
}
/* 星空(CSSグラデで点描) */
.px-stars {
inset: -40px;
background-image:
radial-gradient(1.5px 1.5px at 20% 30%, #fff, transparent),
radial-gradient(1.5px 1.5px at 70% 60%, #cfe2ff, transparent),
radial-gradient(1px 1px at 40% 80%, #fff, transparent),
radial-gradient(1px 1px at 85% 20%, #fff, transparent),
radial-gradient(1.5px 1.5px at 55% 15%, #bcd4ff, transparent),
radial-gradient(1px 1px at 12% 70%, #fff, transparent),
radial-gradient(1.5px 1.5px at 90% 85%, #fff, transparent);
opacity: .8;
}
.px-glow {
width: 420px; height: 420px;
background: radial-gradient(circle, rgba(120,160,255,.25), transparent 65%);
filter: blur(10px);
}
.px-blob {
border-radius: 50%;
filter: blur(2px);
mix-blend-mode: screen;
}
.px-blob--a {
width: 150px; height: 150px;
left: 26%; top: 30%;
background: radial-gradient(circle at 35% 35%, #ff7ac6, #b5179e 70%);
opacity: .75;
}
.px-blob--b {
width: 110px; height: 110px;
right: 24%; bottom: 26%;
background: radial-gradient(circle at 35% 35%, #6ee7ff, #2563eb 70%);
opacity: .8;
}
.px-content {
position: relative;
z-index: 5;
text-align: center;
color: #fff;
will-change: transform;
}
.px-kicker {
font-size: 12px; letter-spacing: .5em; color: #8fb6ff; font-weight: 600;
}
.px-title {
margin: 8px 0 10px;
font-size: 34px; line-height: 1.15; font-weight: 800;
text-shadow: 0 8px 30px rgba(0,0,0,.6);
}
.px-sub { margin: 0; font-size: 13px; color: rgba(255,255,255,.65); }
.px-ring {
width: 250px; height: 250px;
border: 1.5px solid rgba(143,182,255,.4);
border-radius: 50%;
box-shadow: 0 0 40px rgba(143,182,255,.15) inset;
}
@media (prefers-reduced-motion: reduce) {
.px-layer, .px-blob, .px-ring, .px-content { transition: none !important; }
}
JavaScript
// マウス連動パララックス: 各レイヤーを depth に応じてずらす
(() => {
const scene = document.getElementById('parallax');
if (!scene) return; // null安全
const layers = Array.from(scene.querySelectorAll('[data-depth]'));
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let targetX = 0, targetY = 0; // 目標オフセット(-1〜1)
let curX = 0, curY = 0; // 補間中の現在値
// ポインター位置を中心基準の正規化値に変換
const onMove = (e) => {
const rect = scene.getBoundingClientRect();
targetX = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
targetY = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
};
const onLeave = () => { targetX = 0; targetY = 0; };
scene.addEventListener('pointermove', onMove);
scene.addEventListener('pointerleave', onLeave);
const render = () => {
// イージング補間でなめらかに追従
curX += (targetX - curX) * 0.08;
curY += (targetY - curY) * 0.08;
for (const el of layers) {
const d = parseFloat(el.dataset.depth) || 0;
const tx = (-curX * d * 26).toFixed(2);
const ty = (-curY * d * 26).toFixed(2);
el.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
}
requestAnimationFrame(render);
};
if (!reduce) requestAnimationFrame(render);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「マウス連動パララックス層」の効果を追加してください。
# 追加してほしい効果
マウス連動パララックス層(3D & パースペクティブ)
複数レイヤーをマウス位置に応じて異なる量だけ動かし、奥行きを演出します。ヒーローセクションの没入感を高める手法です。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="parallax" id="parallax" aria-label="マウス連動パララックスのデモ">
<!-- data-depth が大きいほど大きく動く -->
<span class="px-layer px-stars" data-depth="0.15"></span>
<span class="px-layer px-glow" data-depth="0.3"></span>
<span class="px-blob px-blob--a" data-depth="0.6"></span>
<span class="px-blob px-blob--b" data-depth="0.9"></span>
<div class="px-content" data-depth="1.4">
<span class="px-kicker">PARALLAX</span>
<h2 class="px-title">奥行きを、<br>マウスで感じる</h2>
<p class="px-sub">カーソルを動かしてみてください</p>
</div>
<span class="px-ring" data-depth="2.2"></span>
</div>
【CSS】
/* ===== マウス連動パララックス層 ===== */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
font-family: "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
.parallax {
position: relative;
width: 100%;
height: 360px;
display: grid;
place-items: center;
background: radial-gradient(circle at 50% 30%, #1d2a55 0%, #0a0f24 55%, #05060f 100%);
cursor: crosshair;
}
.px-layer, .px-blob, .px-ring {
position: absolute;
pointer-events: none;
will-change: transform;
}
/* glow と ring は固定サイズなので inset:0 + margin:auto で親中央に配置。
JS が transform(translate3d) を上書きしても中央基準は崩れない */
.px-glow, .px-ring {
inset: 0;
margin: auto;
}
/* 星空(CSSグラデで点描) */
.px-stars {
inset: -40px;
background-image:
radial-gradient(1.5px 1.5px at 20% 30%, #fff, transparent),
radial-gradient(1.5px 1.5px at 70% 60%, #cfe2ff, transparent),
radial-gradient(1px 1px at 40% 80%, #fff, transparent),
radial-gradient(1px 1px at 85% 20%, #fff, transparent),
radial-gradient(1.5px 1.5px at 55% 15%, #bcd4ff, transparent),
radial-gradient(1px 1px at 12% 70%, #fff, transparent),
radial-gradient(1.5px 1.5px at 90% 85%, #fff, transparent);
opacity: .8;
}
.px-glow {
width: 420px; height: 420px;
background: radial-gradient(circle, rgba(120,160,255,.25), transparent 65%);
filter: blur(10px);
}
.px-blob {
border-radius: 50%;
filter: blur(2px);
mix-blend-mode: screen;
}
.px-blob--a {
width: 150px; height: 150px;
left: 26%; top: 30%;
background: radial-gradient(circle at 35% 35%, #ff7ac6, #b5179e 70%);
opacity: .75;
}
.px-blob--b {
width: 110px; height: 110px;
right: 24%; bottom: 26%;
background: radial-gradient(circle at 35% 35%, #6ee7ff, #2563eb 70%);
opacity: .8;
}
.px-content {
position: relative;
z-index: 5;
text-align: center;
color: #fff;
will-change: transform;
}
.px-kicker {
font-size: 12px; letter-spacing: .5em; color: #8fb6ff; font-weight: 600;
}
.px-title {
margin: 8px 0 10px;
font-size: 34px; line-height: 1.15; font-weight: 800;
text-shadow: 0 8px 30px rgba(0,0,0,.6);
}
.px-sub { margin: 0; font-size: 13px; color: rgba(255,255,255,.65); }
.px-ring {
width: 250px; height: 250px;
border: 1.5px solid rgba(143,182,255,.4);
border-radius: 50%;
box-shadow: 0 0 40px rgba(143,182,255,.15) inset;
}
@media (prefers-reduced-motion: reduce) {
.px-layer, .px-blob, .px-ring, .px-content { transition: none !important; }
}
【JavaScript】
// マウス連動パララックス: 各レイヤーを depth に応じてずらす
(() => {
const scene = document.getElementById('parallax');
if (!scene) return; // null安全
const layers = Array.from(scene.querySelectorAll('[data-depth]'));
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let targetX = 0, targetY = 0; // 目標オフセット(-1〜1)
let curX = 0, curY = 0; // 補間中の現在値
// ポインター位置を中心基準の正規化値に変換
const onMove = (e) => {
const rect = scene.getBoundingClientRect();
targetX = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
targetY = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
};
const onLeave = () => { targetX = 0; targetY = 0; };
scene.addEventListener('pointermove', onMove);
scene.addEventListener('pointerleave', onLeave);
const render = () => {
// イージング補間でなめらかに追従
curX += (targetX - curX) * 0.08;
curY += (targetY - curY) * 0.08;
for (const el of layers) {
const d = parseFloat(el.dataset.depth) || 0;
const tx = (-curX * d * 26).toFixed(2);
const ty = (-curY * d * 26).toFixed(2);
el.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
}
requestAnimationFrame(render);
};
if (!reduce) requestAnimationFrame(render);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。