画像ズーム

商品写真にマウスを乗せて動かすと、その位置を中心に拡大した部分が滑らかに追従するパン&ズーム演出。ECサイトの商品画像を確認する体験を軽量なCSS/JSだけで再現します。

#css#js#gallery

ライブデモ

コード

HTML
<!-- 画像ズーム: 商品画像にhover/マウス移動で拡大鏡的パン&ズーム -->
<div class="zm">
  <div class="zm__frame" id="zmFrame" tabindex="0" aria-label="商品写真。マウスを乗せると拡大部分が動きます">
    <img class="zm__img" id="zmImg" src="https://picsum.photos/id/1039/1000/1000" alt="緑の渓谷に流れる滝(クローズアップ確認用の商品写真)">
    <span class="zm__badge" id="zmBadge" aria-hidden="true">🔍 マウスで拡大</span>
  </div>
  <p class="zm__hint">画像上でマウスを動かすと拡大した部分が追従します(タッチはドラッグ)</p>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  gap: 14px;
  background: linear-gradient(180deg, #faf7f2, #efe9df);
  font-family: "Segoe UI", system-ui, sans-serif;
  padding: 20px;
}

.zm { display: flex; flex-direction: column; align-items: center; gap: 10px; }

.zm__frame {
  --ox: 50%;
  --oy: 50%;
  position: relative;
  width: min(70vw, 340px);
  aspect-ratio: 1 / 1;
  border-radius: 14px;
  overflow: hidden;
  background: #e4ddcf;
  box-shadow: 0 18px 40px -16px rgba(60, 50, 35, .35);
  cursor: zoom-in;
  touch-action: none;
}
.zm__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1);
  transform-origin: var(--ox) var(--oy);
  transition: transform .35s cubic-bezier(.2, .7, .2, 1);
  will-change: transform;
}
.zm__frame.is-zoomed .zm__img { transform: scale(2.3); transition: transform .15s ease-out; }

.zm__badge {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  padding: 5px 12px;
  font-size: 11px;
  letter-spacing: .04em;
  color: #fff;
  background: rgba(30, 26, 18, .6);
  backdrop-filter: blur(3px);
  border-radius: 999px;
  opacity: 1;
  transition: opacity .25s ease;
  pointer-events: none;
}
.zm__frame.is-zoomed .zm__badge { opacity: 0; }

.zm__frame:focus-visible {
  outline: 2px solid #b5893f;
  outline-offset: 3px;
}

.zm__hint {
  margin: 0;
  font-size: 12px;
  color: #7a7062;
  text-align: center;
}
JavaScript
// 画像ズーム: ポインタ位置を transform-origin に反映し、拡大したまま追従パンさせる
(() => {
  const frame = document.getElementById("zmFrame");
  const img = document.getElementById("zmImg");
  if (!frame || !img) return; // null安全

  const setOrigin = (clientX, clientY) => {
    const rect = frame.getBoundingClientRect();
    if (!rect.width || !rect.height) return;
    const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
    const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
    frame.style.setProperty("--ox", `${x}%`);
    frame.style.setProperty("--oy", `${y}%`);
  };

  frame.addEventListener("pointerenter", (e) => {
    frame.classList.add("is-zoomed");
    setOrigin(e.clientX, e.clientY);
  });
  frame.addEventListener("pointermove", (e) => setOrigin(e.clientX, e.clientY));
  frame.addEventListener("pointerleave", () => {
    frame.classList.remove("is-zoomed");
    frame.style.setProperty("--ox", "50%");
    frame.style.setProperty("--oy", "50%");
  });

  // キーボード操作: フォーカスで中央を拡大、離れたら解除(hoverできない環境の代替)
  frame.addEventListener("focus", () => {
    frame.style.setProperty("--ox", "50%");
    frame.style.setProperty("--oy", "50%");
    frame.classList.add("is-zoomed");
  });
  frame.addEventListener("blur", () => frame.classList.remove("is-zoomed"));
  frame.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      frame.classList.toggle("is-zoomed");
    }
  });
})();

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

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

# 追加してほしい効果
画像ズーム(画像 & ギャラリー)
商品写真にマウスを乗せて動かすと、その位置を中心に拡大した部分が滑らかに追従するパン&ズーム演出。ECサイトの商品画像を確認する体験を軽量なCSS/JSだけで再現します。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 画像ズーム: 商品画像にhover/マウス移動で拡大鏡的パン&ズーム -->
<div class="zm">
  <div class="zm__frame" id="zmFrame" tabindex="0" aria-label="商品写真。マウスを乗せると拡大部分が動きます">
    <img class="zm__img" id="zmImg" src="https://picsum.photos/id/1039/1000/1000" alt="緑の渓谷に流れる滝(クローズアップ確認用の商品写真)">
    <span class="zm__badge" id="zmBadge" aria-hidden="true">🔍 マウスで拡大</span>
  </div>
  <p class="zm__hint">画像上でマウスを動かすと拡大した部分が追従します(タッチはドラッグ)</p>
</div>

【CSS】
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  gap: 14px;
  background: linear-gradient(180deg, #faf7f2, #efe9df);
  font-family: "Segoe UI", system-ui, sans-serif;
  padding: 20px;
}

.zm { display: flex; flex-direction: column; align-items: center; gap: 10px; }

.zm__frame {
  --ox: 50%;
  --oy: 50%;
  position: relative;
  width: min(70vw, 340px);
  aspect-ratio: 1 / 1;
  border-radius: 14px;
  overflow: hidden;
  background: #e4ddcf;
  box-shadow: 0 18px 40px -16px rgba(60, 50, 35, .35);
  cursor: zoom-in;
  touch-action: none;
}
.zm__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1);
  transform-origin: var(--ox) var(--oy);
  transition: transform .35s cubic-bezier(.2, .7, .2, 1);
  will-change: transform;
}
.zm__frame.is-zoomed .zm__img { transform: scale(2.3); transition: transform .15s ease-out; }

.zm__badge {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  padding: 5px 12px;
  font-size: 11px;
  letter-spacing: .04em;
  color: #fff;
  background: rgba(30, 26, 18, .6);
  backdrop-filter: blur(3px);
  border-radius: 999px;
  opacity: 1;
  transition: opacity .25s ease;
  pointer-events: none;
}
.zm__frame.is-zoomed .zm__badge { opacity: 0; }

.zm__frame:focus-visible {
  outline: 2px solid #b5893f;
  outline-offset: 3px;
}

.zm__hint {
  margin: 0;
  font-size: 12px;
  color: #7a7062;
  text-align: center;
}

【JavaScript】
// 画像ズーム: ポインタ位置を transform-origin に反映し、拡大したまま追従パンさせる
(() => {
  const frame = document.getElementById("zmFrame");
  const img = document.getElementById("zmImg");
  if (!frame || !img) return; // null安全

  const setOrigin = (clientX, clientY) => {
    const rect = frame.getBoundingClientRect();
    if (!rect.width || !rect.height) return;
    const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
    const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
    frame.style.setProperty("--ox", `${x}%`);
    frame.style.setProperty("--oy", `${y}%`);
  };

  frame.addEventListener("pointerenter", (e) => {
    frame.classList.add("is-zoomed");
    setOrigin(e.clientX, e.clientY);
  });
  frame.addEventListener("pointermove", (e) => setOrigin(e.clientX, e.clientY));
  frame.addEventListener("pointerleave", () => {
    frame.classList.remove("is-zoomed");
    frame.style.setProperty("--ox", "50%");
    frame.style.setProperty("--oy", "50%");
  });

  // キーボード操作: フォーカスで中央を拡大、離れたら解除(hoverできない環境の代替)
  frame.addEventListener("focus", () => {
    frame.style.setProperty("--ox", "50%");
    frame.style.setProperty("--oy", "50%");
    frame.classList.add("is-zoomed");
  });
  frame.addEventListener("blur", () => frame.classList.remove("is-zoomed"));
  frame.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      frame.classList.toggle("is-zoomed");
    }
  });
})();

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

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