clip-path ホバーリビール

clip-path の circle() をマウス位置から広げ、下の画像を円形に切り抜いて見せるホバー演出。商品やビフォーアフターの差分提示に使えます。

#css#clip-path#hover#interactive

ライブデモ

使用例(お題: カフェ MOON BREW)

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

HTML
<!-- MOON BREW:焙煎ビフォーアフターを clip-path 円で見せる商品カード -->
<section class="mb-roast">
  <div class="mb-roast__head">
    <span class="mb-roast__tag">SINGLE ORIGIN</span>
    <h2 class="mb-roast__title">同じ豆、ふたつの焙煎。</h2>
    <p class="mb-roast__lead">カーソルを写真に重ねると、生豆から深煎りへ。<br>あなた好みの一杯を見つけて。</p>
  </div>

  <!-- ホバー位置から円形に深煎り写真が広がる -->
  <figure class="mb-reveal" id="mbReveal" tabindex="0" aria-label="ホバーで焙煎後を表示">
    <img class="mb-reveal__base" src="https://picsum.photos/720/520?random=11" alt="焙煎前の生豆">
    <img class="mb-reveal__top" src="https://picsum.photos/720/520?random=12&grayscale" alt="焙煎後の豆">
    <figcaption class="mb-reveal__cap">☾ HOVER で焙煎</figcaption>
    <span class="mb-reveal__from">生豆 / GREEN</span>
    <span class="mb-reveal__to">深煎り / DARK ROAST</span>
  </figure>
</section>
CSS
/* MOON BREW:焙煎ビフォーアフター clip-path リビール */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: flex;
  align-items: center;
  gap: 26px;
  padding: 0 28px;
  font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(120% 90% at 100% 0%, #3a2818 0%, var(--brown) 60%);
  color: var(--cream);
  overflow: hidden;
}

.mb-roast__head { flex: 0 0 220px; }
.mb-roast__tag {
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--amber);
}
.mb-roast__title {
  margin: 10px 0 12px;
  font-size: 24px;
  line-height: 1.4;
  font-family: "Hiragino Mincho ProN", "Yu Mincho", serif;
}
.mb-roast__lead {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.9;
  color: rgba(245,237,225,0.78);
}

/* リビール枠 */
.mb-reveal {
  position: relative;
  flex: 1;
  height: 300px;
  margin: 0;
  border-radius: 18px;
  overflow: hidden;
  cursor: crosshair;
  box-shadow: 0 18px 40px rgba(0,0,0,0.4);
  outline: none;
}
.mb-reveal__base,
.mb-reveal__top {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* 上層(深煎り)を琥珀トーンに寄せ、円形に切り抜いて出現 */
.mb-reveal__top {
  filter: sepia(0.5) saturate(1.4) brightness(0.85) contrast(1.1);
  clip-path: circle(0 at 50% 50%);
  transition: clip-path 0.12s ease-out;
}
.mb-reveal:hover .mb-reveal__top,
.mb-reveal:focus .mb-reveal__top {
  clip-path: circle(120px at var(--mx, 50%) var(--my, 50%));
}

.mb-reveal__cap {
  position: absolute;
  left: 14px;
  bottom: 14px;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 11px;
  letter-spacing: 0.1em;
  background: rgba(43,29,18,0.55);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--cream);
}
.mb-reveal__from,
.mb-reveal__to {
  position: absolute;
  top: 14px;
  font-size: 10px;
  letter-spacing: 0.15em;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(0,0,0,0.5);
}
.mb-reveal__from { left: 14px; }
.mb-reveal__to { right: 14px; background: rgba(201,138,59,0.85); color: #2b1d12; font-weight: 700; }
JavaScript
// マウス位置を CSS変数へ渡し、clip-path 円の中心を追従させる
const reveal = document.getElementById("mbReveal");

if (reveal) {
  const move = (clientX, clientY) => {
    const r = reveal.getBoundingClientRect();
    if (!r.width || !r.height) return;
    // 枠内の相対位置を % で算出
    const x = ((clientX - r.left) / r.width) * 100;
    const y = ((clientY - r.top) / r.height) * 100;
    reveal.style.setProperty("--mx", `${x}%`);
    reveal.style.setProperty("--my", `${y}%`);
  };
  reveal.addEventListener("pointermove", (e) => move(e.clientX, e.clientY));
  // キーボードフォーカス時は中央に出す
  reveal.addEventListener("focus", () => {
    reveal.style.setProperty("--mx", "50%");
    reveal.style.setProperty("--my", "50%");
  });
}

コード

HTML
<!-- clip-path で2枚の画像をホバー切り替え -->
<div class="stage">
  <figure class="reveal" tabindex="0" aria-label="ホバーで第2画像を表示">
    <!-- 下層: 通常時に見える画像 -->
    <img class="reveal__base" src="https://picsum.photos/id/1015/800/600" alt="風景(通常)">
    <!-- 上層: clip-path で出現するモノクロ画像 -->
    <img class="reveal__top" src="https://picsum.photos/id/1015/800/600?grayscale" alt="風景(モノクロ)">
    <figcaption class="reveal__cap">HOVER ME</figcaption>
  </figure>
</div>
CSS
/* ステージ全体 */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 30% 20%, #2a2d4a, #14151f 70%);
  font-family: "Segoe UI", system-ui, sans-serif;
}
.stage { padding: 24px; }

/* 画像枠 */
.reveal {
  position: relative;
  width: min(64vw, 380px);
  aspect-ratio: 4 / 3;
  margin: 0;
  border-radius: 16px;
  overflow: hidden;
  cursor: pointer;
  box-shadow: 0 20px 50px -15px rgba(0, 0, 0, .6);
  outline: none;
}
.reveal img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 上層は斜めの clip-path で隠しておく */
.reveal__top {
  /* マウス位置を CSS 変数で受け取り、円形に切り抜く */
  clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
  transition: clip-path .45s cubic-bezier(.22, 1, .36, 1);
  filter: saturate(1.1) contrast(1.05);
}

/* ホバー / フォーカスで広く切り抜く */
.reveal:hover .reveal__top,
.reveal:focus-visible .reveal__top {
  clip-path: circle(75% at var(--x, 50%) var(--y, 50%));
}

/* キャプション */
.reveal__cap {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  padding: 8px 18px;
  font-size: 13px;
  letter-spacing: .25em;
  font-weight: 700;
  color: #fff;
  background: rgba(20, 21, 31, .55);
  backdrop-filter: blur(6px);
  border-radius: 999px;
  transition: opacity .3s ease;
}
.reveal:hover .reveal__cap { opacity: 0; }

/* フォーカスリング */
.reveal:focus-visible { box-shadow: 0 0 0 3px #8ab4ff, 0 20px 50px -15px rgba(0, 0, 0, .6); }

@media (prefers-reduced-motion: reduce) {
  .reveal__top { transition: none; }
}
JavaScript
// マウス位置を CSS 変数(--x/--y)へ反映し、その地点から円形リビールさせる
const reveal = document.querySelector(".reveal");

if (reveal) {
  // 枠内の相対位置を % で算出して変数に書き込む
  const move = (clientX, clientY) => {
    const r = reveal.getBoundingClientRect();
    const x = ((clientX - r.left) / r.width) * 100;
    const y = ((clientY - r.top) / r.height) * 100;
    reveal.style.setProperty("--x", `${x.toFixed(1)}%`);
    reveal.style.setProperty("--y", `${y.toFixed(1)}%`);
  };

  reveal.addEventListener("pointermove", (e) => move(e.clientX, e.clientY));

  // キーボード操作(フォーカス)時は中央から開く
  reveal.addEventListener("focus", () => {
    reveal.style.setProperty("--x", "50%");
    reveal.style.setProperty("--y", "50%");
  });
}

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

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

# 追加してほしい効果
clip-path ホバーリビール(画像エフェクト)
clip-path の circle() をマウス位置から広げ、下の画像を円形に切り抜いて見せるホバー演出。商品やビフォーアフターの差分提示に使えます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- clip-path で2枚の画像をホバー切り替え -->
<div class="stage">
  <figure class="reveal" tabindex="0" aria-label="ホバーで第2画像を表示">
    <!-- 下層: 通常時に見える画像 -->
    <img class="reveal__base" src="https://picsum.photos/id/1015/800/600" alt="風景(通常)">
    <!-- 上層: clip-path で出現するモノクロ画像 -->
    <img class="reveal__top" src="https://picsum.photos/id/1015/800/600?grayscale" alt="風景(モノクロ)">
    <figcaption class="reveal__cap">HOVER ME</figcaption>
  </figure>
</div>

【CSS】
/* ステージ全体 */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 30% 20%, #2a2d4a, #14151f 70%);
  font-family: "Segoe UI", system-ui, sans-serif;
}
.stage { padding: 24px; }

/* 画像枠 */
.reveal {
  position: relative;
  width: min(64vw, 380px);
  aspect-ratio: 4 / 3;
  margin: 0;
  border-radius: 16px;
  overflow: hidden;
  cursor: pointer;
  box-shadow: 0 20px 50px -15px rgba(0, 0, 0, .6);
  outline: none;
}
.reveal img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 上層は斜めの clip-path で隠しておく */
.reveal__top {
  /* マウス位置を CSS 変数で受け取り、円形に切り抜く */
  clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
  transition: clip-path .45s cubic-bezier(.22, 1, .36, 1);
  filter: saturate(1.1) contrast(1.05);
}

/* ホバー / フォーカスで広く切り抜く */
.reveal:hover .reveal__top,
.reveal:focus-visible .reveal__top {
  clip-path: circle(75% at var(--x, 50%) var(--y, 50%));
}

/* キャプション */
.reveal__cap {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  padding: 8px 18px;
  font-size: 13px;
  letter-spacing: .25em;
  font-weight: 700;
  color: #fff;
  background: rgba(20, 21, 31, .55);
  backdrop-filter: blur(6px);
  border-radius: 999px;
  transition: opacity .3s ease;
}
.reveal:hover .reveal__cap { opacity: 0; }

/* フォーカスリング */
.reveal:focus-visible { box-shadow: 0 0 0 3px #8ab4ff, 0 20px 50px -15px rgba(0, 0, 0, .6); }

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

【JavaScript】
// マウス位置を CSS 変数(--x/--y)へ反映し、その地点から円形リビールさせる
const reveal = document.querySelector(".reveal");

if (reveal) {
  // 枠内の相対位置を % で算出して変数に書き込む
  const move = (clientX, clientY) => {
    const r = reveal.getBoundingClientRect();
    const x = ((clientX - r.left) / r.width) * 100;
    const y = ((clientY - r.top) / r.height) * 100;
    reveal.style.setProperty("--x", `${x.toFixed(1)}%`);
    reveal.style.setProperty("--y", `${y.toFixed(1)}%`);
  };

  reveal.addEventListener("pointermove", (e) => move(e.clientX, e.clientY));

  // キーボード操作(フォーカス)時は中央から開く
  reveal.addEventListener("focus", () => {
    reveal.style.setProperty("--x", "50%");
    reveal.style.setProperty("--y", "50%");
  });
}

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

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