ビフォーアフター比較

中央のハンドルをドラッグして2枚の写真を左右に比較できるスライダー。矢印キーにも対応し、施工事例やレタッチ前後の違いを直感的に伝えられます。

#css#js#gallery

ライブデモ

コード

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