マウス連動パララックス層

複数レイヤーをマウス位置に応じて異なる量だけ動かし、奥行きを演出します。ヒーローセクションの没入感を高める手法です。

#javascript#parallax#interaction#animation

ライブデモ

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