画像ズーム
商品写真にマウスを乗せて動かすと、その位置を中心に拡大した部分が滑らかに追従するパン&ズーム演出。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");
}
});
})();
🤖 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。