ビフォーアフター比較
中央のハンドルをドラッグして2枚の写真を左右に比較できるスライダー。矢印キーにも対応し、施工事例やレタッチ前後の違いを直感的に伝えられます。
ライブデモ
コード
HTML
<!-- ビフォーアフター比較: 中央ハンドルをドラッグ(または矢印キー)で2画像を左右比較 -->
<div class="gba">
<div class="gba__frame" id="gbaFrame">
<img class="gba__img gba__img--after" src="https://picsum.photos/id/1074/900/600" alt="仕上げ後の写真(ライオンの横顔・カラー)">
<div class="gba__before" id="gbaBefore">
<img class="gba__img" src="https://picsum.photos/id/1074/900/600?grayscale" alt="仕上げ前の写真(ライオンの横顔・モノクロ)">
</div>
<span class="gba__tag gba__tag--before" aria-hidden="true">BEFORE</span>
<span class="gba__tag gba__tag--after" aria-hidden="true">AFTER</span>
<div class="gba__line" id="gbaLine" aria-hidden="true"></div>
<div class="gba__handle" id="gbaHandle" role="slider" tabindex="0"
aria-label="比較位置(左でビフォー、右でアフター)"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-orientation="horizontal">
<span class="gba__arrow gba__arrow--l" aria-hidden="true">‹</span>
<span class="gba__arrow gba__arrow--r" aria-hidden="true">›</span>
</div>
</div>
<p class="gba__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, #eef0f1, #dfe2e6);
font-family: "Segoe UI", system-ui, sans-serif;
padding: 20px;
}
.gba { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.gba__frame {
--pos: 50%;
position: relative;
width: min(88vw, 460px);
aspect-ratio: 3 / 2;
border-radius: 16px;
overflow: hidden;
user-select: none;
touch-action: none;
background: #cfd3d8;
box-shadow: 0 22px 48px -18px rgba(30, 36, 46, .45);
cursor: ew-resize;
}
.gba__img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
/* before レイヤーは右端を clip して幅を可変に(--pos に追従) */
.gba__before {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}
.gba__tag {
position: absolute;
top: 12px;
z-index: 3;
padding: 4px 12px;
font-size: 10.5px;
letter-spacing: .18em;
font-weight: 800;
color: #fff;
background: rgba(20, 22, 26, .55);
backdrop-filter: blur(4px);
border-radius: 999px;
pointer-events: none;
}
.gba__tag--before { left: 12px; }
.gba__tag--after { right: 12px; }
.gba__line {
position: absolute;
top: 0;
bottom: 0;
left: var(--pos);
width: 2px;
background: #fff;
transform: translateX(-1px);
z-index: 2;
pointer-events: none;
box-shadow: 0 0 10px rgba(0, 0, 0, .35);
}
.gba__handle {
position: absolute;
top: 50%;
left: var(--pos);
transform: translate(-50%, -50%);
z-index: 4;
width: 42px;
height: 42px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
box-shadow: 0 6px 16px rgba(20, 22, 26, .4);
cursor: ew-resize;
}
.gba__arrow { font-size: 12px; color: #2a2e35; line-height: 1; }
.gba__handle:focus-visible {
outline: 3px solid #5b8def;
outline-offset: 3px;
}
.gba__hint {
margin: 0;
font-size: 12px;
color: #5b6270;
text-align: center;
}
JavaScript
// ビフォーアフター: フレーム全体をドラッグ可能にし、--pos(%) に応じて before レイヤーとハンドルを同期させる
(() => {
const frame = document.getElementById("gbaFrame");
const handle = document.getElementById("gbaHandle");
if (!frame || !handle) return; // null安全
let pos = 50;
let dragging = false;
const setPos = (value) => {
pos = Math.min(100, Math.max(0, value));
frame.style.setProperty("--pos", `${pos}%`);
handle.setAttribute("aria-valuenow", String(Math.round(pos)));
};
const posFromClientX = (clientX) => {
const rect = frame.getBoundingClientRect();
if (!rect.width) return pos;
return ((clientX - rect.left) / rect.width) * 100;
};
const onPointerDown = (e) => {
dragging = true;
frame.setPointerCapture && frame.setPointerCapture(e.pointerId);
setPos(posFromClientX(e.clientX));
handle.focus();
};
const onPointerMove = (e) => {
if (!dragging) return;
setPos(posFromClientX(e.clientX));
};
const onPointerUp = (e) => {
dragging = false;
frame.releasePointerCapture && e.pointerId != null && frame.releasePointerCapture(e.pointerId);
};
frame.addEventListener("pointerdown", onPointerDown);
frame.addEventListener("pointermove", onPointerMove);
frame.addEventListener("pointerup", onPointerUp);
frame.addEventListener("pointercancel", onPointerUp);
// キーボード操作(矢印キーで5%刻み、Home/Endで両端)
handle.addEventListener("keydown", (e) => {
const step = 5;
if (e.key === "ArrowLeft" || e.key === "ArrowDown") { e.preventDefault(); setPos(pos - step); }
else if (e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); setPos(pos + step); }
else if (e.key === "Home") { e.preventDefault(); setPos(0); }
else if (e.key === "End") { e.preventDefault(); setPos(100); }
});
setPos(50); // 初期反映
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ビフォーアフター比較」の効果を追加してください。
# 追加してほしい効果
ビフォーアフター比較(画像 & ギャラリー)
中央のハンドルをドラッグして2枚の写真を左右に比較できるスライダー。矢印キーにも対応し、施工事例やレタッチ前後の違いを直感的に伝えられます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ビフォーアフター比較: 中央ハンドルをドラッグ(または矢印キー)で2画像を左右比較 -->
<div class="gba">
<div class="gba__frame" id="gbaFrame">
<img class="gba__img gba__img--after" src="https://picsum.photos/id/1074/900/600" alt="仕上げ後の写真(ライオンの横顔・カラー)">
<div class="gba__before" id="gbaBefore">
<img class="gba__img" src="https://picsum.photos/id/1074/900/600?grayscale" alt="仕上げ前の写真(ライオンの横顔・モノクロ)">
</div>
<span class="gba__tag gba__tag--before" aria-hidden="true">BEFORE</span>
<span class="gba__tag gba__tag--after" aria-hidden="true">AFTER</span>
<div class="gba__line" id="gbaLine" aria-hidden="true"></div>
<div class="gba__handle" id="gbaHandle" role="slider" tabindex="0"
aria-label="比較位置(左でビフォー、右でアフター)"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-orientation="horizontal">
<span class="gba__arrow gba__arrow--l" aria-hidden="true">‹</span>
<span class="gba__arrow gba__arrow--r" aria-hidden="true">›</span>
</div>
</div>
<p class="gba__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, #eef0f1, #dfe2e6);
font-family: "Segoe UI", system-ui, sans-serif;
padding: 20px;
}
.gba { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.gba__frame {
--pos: 50%;
position: relative;
width: min(88vw, 460px);
aspect-ratio: 3 / 2;
border-radius: 16px;
overflow: hidden;
user-select: none;
touch-action: none;
background: #cfd3d8;
box-shadow: 0 22px 48px -18px rgba(30, 36, 46, .45);
cursor: ew-resize;
}
.gba__img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
/* before レイヤーは右端を clip して幅を可変に(--pos に追従) */
.gba__before {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}
.gba__tag {
position: absolute;
top: 12px;
z-index: 3;
padding: 4px 12px;
font-size: 10.5px;
letter-spacing: .18em;
font-weight: 800;
color: #fff;
background: rgba(20, 22, 26, .55);
backdrop-filter: blur(4px);
border-radius: 999px;
pointer-events: none;
}
.gba__tag--before { left: 12px; }
.gba__tag--after { right: 12px; }
.gba__line {
position: absolute;
top: 0;
bottom: 0;
left: var(--pos);
width: 2px;
background: #fff;
transform: translateX(-1px);
z-index: 2;
pointer-events: none;
box-shadow: 0 0 10px rgba(0, 0, 0, .35);
}
.gba__handle {
position: absolute;
top: 50%;
left: var(--pos);
transform: translate(-50%, -50%);
z-index: 4;
width: 42px;
height: 42px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
box-shadow: 0 6px 16px rgba(20, 22, 26, .4);
cursor: ew-resize;
}
.gba__arrow { font-size: 12px; color: #2a2e35; line-height: 1; }
.gba__handle:focus-visible {
outline: 3px solid #5b8def;
outline-offset: 3px;
}
.gba__hint {
margin: 0;
font-size: 12px;
color: #5b6270;
text-align: center;
}
【JavaScript】
// ビフォーアフター: フレーム全体をドラッグ可能にし、--pos(%) に応じて before レイヤーとハンドルを同期させる
(() => {
const frame = document.getElementById("gbaFrame");
const handle = document.getElementById("gbaHandle");
if (!frame || !handle) return; // null安全
let pos = 50;
let dragging = false;
const setPos = (value) => {
pos = Math.min(100, Math.max(0, value));
frame.style.setProperty("--pos", `${pos}%`);
handle.setAttribute("aria-valuenow", String(Math.round(pos)));
};
const posFromClientX = (clientX) => {
const rect = frame.getBoundingClientRect();
if (!rect.width) return pos;
return ((clientX - rect.left) / rect.width) * 100;
};
const onPointerDown = (e) => {
dragging = true;
frame.setPointerCapture && frame.setPointerCapture(e.pointerId);
setPos(posFromClientX(e.clientX));
handle.focus();
};
const onPointerMove = (e) => {
if (!dragging) return;
setPos(posFromClientX(e.clientX));
};
const onPointerUp = (e) => {
dragging = false;
frame.releasePointerCapture && e.pointerId != null && frame.releasePointerCapture(e.pointerId);
};
frame.addEventListener("pointerdown", onPointerDown);
frame.addEventListener("pointermove", onPointerMove);
frame.addEventListener("pointerup", onPointerUp);
frame.addEventListener("pointercancel", onPointerUp);
// キーボード操作(矢印キーで5%刻み、Home/Endで両端)
handle.addEventListener("keydown", (e) => {
const step = 5;
if (e.key === "ArrowLeft" || e.key === "ArrowDown") { e.preventDefault(); setPos(pos - step); }
else if (e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); setPos(pos + step); }
else if (e.key === "Home") { e.preventDefault(); setPos(0); }
else if (e.key === "End") { e.preventDefault(); setPos(100); }
});
setPos(50); // 初期反映
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。