奥行きレイヤースタック

同じ図形をZ軸方向に積層し、マウスで覗き込むように傾けて立体感を出すデモ。ロゴやキービジュアルの演出に応用できます。

#css#javascript#3d#interaction

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:ロゴマークをZ軸に積層したキービジュアル -->
<section class="fd-kv" aria-label="FlowDesk ブランドロゴ">
  <div class="fd-kv__scene" id="fdScene">
    <!-- 同じロゴ形状をZ方向に積層して立体感を出す -->
    <div class="fd-stack" id="fdStack">
      <span class="fd-mark" style="--z:0"></span>
      <span class="fd-mark" style="--z:1"></span>
      <span class="fd-mark" style="--z:2"></span>
      <span class="fd-mark" style="--z:3"></span>
      <span class="fd-mark fd-mark--top" style="--z:4"></span>
    </div>
  </div>

  <div class="fd-kv__copy">
    <span class="fd-kv__brand">▰ FlowDesk</span>
    <h1 class="fd-kv__title">流れを、設計する。</h1>
    <p class="fd-kv__lead">チームの業務フローを可視化し、自動化するワークOS。</p>
    <span class="fd-kv__hint">マウスで覗き込むように傾きます</span>
  </div>
</section>
CSS
/* FlowDesk:ロゴのZ軸レイヤースタック */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(circle at 30% 30%, #1b2c52 0%, var(--navy) 65%);
  color: var(--white);
  overflow: hidden;
}

/* 左:3Dシーン。perspective が立体の肝 */
.fd-kv__scene {
  height: 100%;
  display: grid;
  place-items: center;
  perspective: 800px;
}

.fd-stack {
  position: relative;
  width: 150px;
  height: 150px;
  transform-style: preserve-3d; /* 子レイヤーを3D空間へ */
  transform: rotateX(18deg) rotateY(-22deg);
  transition: transform 0.2s ease-out;
}

/* 各ロゴ層:--z でZ位置と濃さを変える */
.fd-mark {
  position: absolute;
  inset: 0;
  border-radius: 26px;
  background: linear-gradient(135deg, #3a63e0, var(--blue));
  border: 1px solid rgba(255,255,255,.25);
  transform: translateZ(calc(var(--z) * 20px));
  opacity: calc(0.35 + var(--z) * 0.14);
  box-shadow: 0 10px 24px rgba(0,0,0,.25);
}
/* 中央に切り抜き風の窓を作りロゴ感を出す */
.fd-mark::before {
  content: "";
  position: absolute;
  inset: 34%;
  border-radius: 8px;
  background: var(--navy);
}
.fd-mark--top {
  background: linear-gradient(135deg, #6e93ff, #4f7cff);
  box-shadow: 0 16px 34px rgba(79,124,255,.5);
}
.fd-mark--top::before { background: linear-gradient(135deg, #fff, #d7e1ff); }

/* 右:コピー */
.fd-kv__copy { padding: 0 30px 0 8px; }
.fd-kv__brand { font-size: 13px; font-weight: 800; letter-spacing: 0.1em; color: var(--blue); }
.fd-kv__title { margin: 12px 0 8px; font-size: 30px; font-weight: 900; letter-spacing: 0.02em; }
.fd-kv__lead { margin: 0 0 14px; font-size: 13px; line-height: 1.8; color: rgba(255,255,255,.72); }
.fd-kv__hint { font-size: 11px; color: rgba(255,255,255,.45); }

@media (prefers-reduced-motion: reduce) {
  .fd-stack { transition: none; }
}
JavaScript
// FlowDesk レイヤースタック:マウス位置でスタック全体を覗き込むように傾ける
(() => {
  const scene = document.getElementById("fdScene");
  const stack = document.getElementById("fdStack");
  if (!scene || !stack) return; // null安全

  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  const baseX = 18, baseY = -22; // 初期傾き
  const RANGE = 22;              // 振れ幅(度)

  scene.addEventListener("pointermove", (e) => {
    if (reduce) return;
    const r = scene.getBoundingClientRect();
    const nx = (e.clientX - r.left) / r.width - 0.5;  // -0.5〜0.5
    const ny = (e.clientY - r.top) / r.height - 0.5;
    const rx = baseX - ny * RANGE;
    const ry = baseY + nx * RANGE;
    stack.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
  });

  // 離れたら初期姿勢へ戻す
  scene.addEventListener("pointerleave", () => {
    stack.style.transform = `rotateX(${baseX}deg) rotateY(${baseY}deg)`;
  });
})();

コード

HTML
<div class="lstack-scene" aria-label="奥行きレイヤースタックのデモ">
  <div class="lstack" id="lstack" tabindex="0" role="img" aria-label="重なった3Dレイヤー">
    <!-- 同じ図形を奥行き方向に積層して立体感を出す -->
    <span class="lstack__layer" style="--i:0"></span>
    <span class="lstack__layer" style="--i:1"></span>
    <span class="lstack__layer" style="--i:2"></span>
    <span class="lstack__layer" style="--i:3"></span>
    <span class="lstack__layer" style="--i:4"></span>
    <span class="lstack__layer lstack__layer--top" style="--i:5">3D</span>
  </div>
  <p class="lstack-hint">マウスで覗き込む</p>
</div>
CSS
/* ===== 奥行きレイヤースタック ===== */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 40%, #18243f 0%, #070b16 60%, #03050b 100%);
  overflow: hidden;
}

.lstack-scene { display: grid; place-items: center; gap: 16px; }

.lstack {
  position: relative;
  width: 180px;
  height: 180px;
  perspective: 900px;
  transform-style: preserve-3d;
  outline: none;
}

.lstack__layer {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 150px;
  height: 150px;
  border-radius: 26px;
  border: 1.5px solid hsl(calc(200 + var(--i) * 24) 90% 70% / .85);
  background: hsl(calc(200 + var(--i) * 24) 80% 55% / .10);
  box-shadow: 0 0 24px hsl(calc(200 + var(--i) * 24) 90% 60% / .25);
  /* 各レイヤーを奥行き方向にずらす(--rx/--ry はJSが更新) */
  transform:
    rotateX(var(--rx, 14deg)) rotateY(var(--ry, -18deg))
    translateZ(calc(var(--i) * 26px));
  transition: transform .12s ease-out;
  display: grid;
  place-items: center;
}

.lstack__layer--top {
  font-size: 56px;
  font-weight: 900;
  color: #fff;
  letter-spacing: .04em;
  background: hsl(320 80% 58% / .14);
  border-color: hsl(320 90% 75% / .9);
  text-shadow: 0 6px 18px rgba(0,0,0,.5);
}

.lstack-hint {
  margin: 0;
  font-size: 12px; letter-spacing: .2em;
  color: rgba(180,200,255,.6);
}

@media (prefers-reduced-motion: reduce) {
  .lstack__layer { transition: none; }
}
JavaScript
// 奥行きレイヤースタック: マウスで全レイヤーの傾きを連動更新
(() => {
  const stack = document.getElementById('lstack');
  if (!stack) return; // null安全

  const layers = Array.from(stack.querySelectorAll('.lstack__layer'));
  if (layers.length === 0) return;

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let tRx = 14, tRy = -18;   // 目標角度
  let rx = 14, ry = -18;     // 現在角度(補間用)

  const onMove = (e) => {
    const r = stack.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width - 0.5;
    const py = (e.clientY - r.top) / r.height - 0.5;
    tRy = px * 50;            // 左右で最大±25度
    tRx = -py * 50 + 4;       // 上下
  };
  const onLeave = () => { tRx = 14; tRy = -18; };

  // 親要素全体でポインターを拾う(画面端でも反応)
  document.addEventListener('pointermove', onMove);
  stack.addEventListener('pointerleave', onLeave);

  const apply = () => {
    rx += (tRx - rx) * 0.1;
    ry += (tRy - ry) * 0.1;
    for (const el of layers) {
      el.style.setProperty('--rx', rx.toFixed(2) + 'deg');
      el.style.setProperty('--ry', ry.toFixed(2) + 'deg');
    }
    requestAnimationFrame(apply);
  };

  if (!reduce) requestAnimationFrame(apply);
})();

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

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

# 追加してほしい効果
奥行きレイヤースタック(3D & パースペクティブ)
同じ図形をZ軸方向に積層し、マウスで覗き込むように傾けて立体感を出すデモ。ロゴやキービジュアルの演出に応用できます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="lstack-scene" aria-label="奥行きレイヤースタックのデモ">
  <div class="lstack" id="lstack" tabindex="0" role="img" aria-label="重なった3Dレイヤー">
    <!-- 同じ図形を奥行き方向に積層して立体感を出す -->
    <span class="lstack__layer" style="--i:0"></span>
    <span class="lstack__layer" style="--i:1"></span>
    <span class="lstack__layer" style="--i:2"></span>
    <span class="lstack__layer" style="--i:3"></span>
    <span class="lstack__layer" style="--i:4"></span>
    <span class="lstack__layer lstack__layer--top" style="--i:5">3D</span>
  </div>
  <p class="lstack-hint">マウスで覗き込む</p>
</div>

【CSS】
/* ===== 奥行きレイヤースタック ===== */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 40%, #18243f 0%, #070b16 60%, #03050b 100%);
  overflow: hidden;
}

.lstack-scene { display: grid; place-items: center; gap: 16px; }

.lstack {
  position: relative;
  width: 180px;
  height: 180px;
  perspective: 900px;
  transform-style: preserve-3d;
  outline: none;
}

.lstack__layer {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 150px;
  height: 150px;
  border-radius: 26px;
  border: 1.5px solid hsl(calc(200 + var(--i) * 24) 90% 70% / .85);
  background: hsl(calc(200 + var(--i) * 24) 80% 55% / .10);
  box-shadow: 0 0 24px hsl(calc(200 + var(--i) * 24) 90% 60% / .25);
  /* 各レイヤーを奥行き方向にずらす(--rx/--ry はJSが更新) */
  transform:
    rotateX(var(--rx, 14deg)) rotateY(var(--ry, -18deg))
    translateZ(calc(var(--i) * 26px));
  transition: transform .12s ease-out;
  display: grid;
  place-items: center;
}

.lstack__layer--top {
  font-size: 56px;
  font-weight: 900;
  color: #fff;
  letter-spacing: .04em;
  background: hsl(320 80% 58% / .14);
  border-color: hsl(320 90% 75% / .9);
  text-shadow: 0 6px 18px rgba(0,0,0,.5);
}

.lstack-hint {
  margin: 0;
  font-size: 12px; letter-spacing: .2em;
  color: rgba(180,200,255,.6);
}

@media (prefers-reduced-motion: reduce) {
  .lstack__layer { transition: none; }
}

【JavaScript】
// 奥行きレイヤースタック: マウスで全レイヤーの傾きを連動更新
(() => {
  const stack = document.getElementById('lstack');
  if (!stack) return; // null安全

  const layers = Array.from(stack.querySelectorAll('.lstack__layer'));
  if (layers.length === 0) return;

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let tRx = 14, tRy = -18;   // 目標角度
  let rx = 14, ry = -18;     // 現在角度(補間用)

  const onMove = (e) => {
    const r = stack.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width - 0.5;
    const py = (e.clientY - r.top) / r.height - 0.5;
    tRy = px * 50;            // 左右で最大±25度
    tRx = -py * 50 + 4;       // 上下
  };
  const onLeave = () => { tRx = 14; tRy = -18; };

  // 親要素全体でポインターを拾う(画面端でも反応)
  document.addEventListener('pointermove', onMove);
  stack.addEventListener('pointerleave', onLeave);

  const apply = () => {
    rx += (tRx - rx) * 0.1;
    ry += (tRy - ry) * 0.1;
    for (const el of layers) {
      el.style.setProperty('--rx', rx.toFixed(2) + 'deg');
      el.style.setProperty('--ry', ry.toFixed(2) + 'deg');
    }
    requestAnimationFrame(apply);
  };

  if (!reduce) requestAnimationFrame(apply);
})();

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

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