クリップパス画像リビール

前面に次の画像を載せ clip-path の斜めワイプで切替えるギャラリー遷移。完了後に背面へ確定する二層構成で、無段差にスライドし続けます。

#css#javascript#clip-path#gallery

ライブデモ

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

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

HTML
<!-- MOON BREW: メニュー写真ギャラリーを clip-path の斜めワイプで切替 -->
<div class="mr-stage">
  <div class="mr-head">
    <span class="mr-logo">☕ MOON BREW</span>
    <span class="mr-sub">本日のおすすめ</span>
  </div>

  <div class="mr-frame">
    <!-- 2枚を重ね、前面を斜めワイプで露出 -->
    <div class="mr-layer mr-layer--back" style="background-image:url(https://picsum.photos/720/500?random=41)"></div>
    <div class="mr-layer mr-layer--front"></div>
    <div class="mr-shade" aria-hidden="true"></div>

    <div class="mr-caption">
      <span class="mr-index">01 / 04</span>
      <h2 class="mr-name">ムーンラテ</h2>
      <span class="mr-price">¥620</span>
    </div>
  </div>

  <div class="mr-thumbs" role="tablist" aria-label="メニュー選択">
    <button class="mr-thumb is-on" data-img="https://picsum.photos/720/500?random=41" data-name="ムーンラテ" data-price="¥620" style="background-image:url(https://picsum.photos/120/90?random=41)" aria-label="ムーンラテ"></button>
    <button class="mr-thumb" data-img="https://picsum.photos/720/500?random=42" data-name="琥珀のカフェモカ" data-price="¥680" style="background-image:url(https://picsum.photos/120/90?random=42)" aria-label="琥珀のカフェモカ"></button>
    <button class="mr-thumb" data-img="https://picsum.photos/720/500?random=43" data-name="焼きたてスコーン" data-price="¥420" style="background-image:url(https://picsum.photos/120/90?random=43)" aria-label="焼きたてスコーン"></button>
    <button class="mr-thumb" data-img="https://picsum.photos/720/500?random=44" data-name="季節のタルト" data-price="¥560" style="background-image:url(https://picsum.photos/120/90?random=44)" aria-label="季節のタルト"></button>
  </div>
</div>
CSS
* { box-sizing: border-box; }

:root { --ease: cubic-bezier(.76, 0, .24, 1); }

body {
  margin: 0;
  min-height: 400px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Mincho ProN", "Georgia", serif;
  color: #f5ede1;
  background: #1c120a;
}

.mr-stage { width: min(560px, 94vw); }

.mr-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 4px 12px;
  font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
}
.mr-logo { font-size: 17px; font-weight: 700; color: #f5ede1; letter-spacing: .06em; }
.mr-sub { font-size: 12px; color: #c98a3b; letter-spacing: .15em; }

.mr-frame {
  position: relative;
  height: 240px;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 20px 50px rgba(0, 0, 0, .55);
}

.mr-layer {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}
/* 前面レイヤーを斜めワイプ */
.mr-layer--front {
  clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
  transition: clip-path .62s var(--ease);
}
.mr-frame.is-revealing .mr-layer--front {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

/* 下部に文字を読みやすくする影 */
.mr-shade {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(28, 18, 10, .78) 0%, transparent 48%);
  pointer-events: none;
}

.mr-caption {
  position: absolute;
  left: 24px;
  bottom: 20px;
  z-index: 2;
  text-shadow: 0 2px 12px rgba(0, 0, 0, .6);
}
.mr-index { font-size: 11px; letter-spacing: .25em; color: #e7c79a; }
.mr-name { margin: 5px 0 0; font-size: 28px; }
.mr-price { display: inline-block; margin-top: 4px; font-size: 15px; color: #f0c98a; font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif; }

.mr-thumbs { display: flex; gap: 11px; margin-top: 14px; justify-content: center; }
.mr-thumb {
  width: 60px;
  height: 44px;
  border-radius: 9px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  background-size: cover;
  background-position: center;
  opacity: .55;
  transition: opacity .25s ease, transform .25s ease, border-color .25s ease;
}
.mr-thumb:hover { opacity: .85; transform: translateY(-2px); }
.mr-thumb.is-on { opacity: 1; border-color: #c98a3b; transform: translateY(-2px); }
.mr-thumb:focus-visible { outline: 2px solid #c98a3b; outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .mr-layer--front { transition-duration: 1ms; }
}
JavaScript
// MOON BREW メニュー写真を clip-path の斜めワイプで切替(露出後に背面へ確定)
(() => {
  const frame = document.querySelector('.mr-frame');
  const back = document.querySelector('.mr-layer--back');
  const front = document.querySelector('.mr-layer--front');
  const nameEl = document.querySelector('.mr-name');
  const priceEl = document.querySelector('.mr-price');
  const indexEl = document.querySelector('.mr-index');
  const thumbs = [...document.querySelectorAll('.mr-thumb')];
  if (!frame || !back || !front || thumbs.length === 0) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let current = 0;
  let busy = false;
  const total = String(thumbs.length).padStart(2, '0');

  const show = (index) => {
    if (busy || index === current) return;
    const thumb = thumbs[index];
    if (!thumb) return;
    busy = true;

    const img = thumb.dataset.img;
    const name = thumb.dataset.name ?? '';
    const price = thumb.dataset.price ?? '';

    // 前面に次の写真を仕込み、ワイプで露出
    front.style.backgroundImage = `url(${img})`;
    nameEl.textContent = name;
    if (priceEl) priceEl.textContent = price;
    indexEl.textContent = `${String(index + 1).padStart(2, '0')} / ${total}`;
    thumbs.forEach((t, i) => t.classList.toggle('is-on', i === index));

    const finish = () => {
      // 露出した写真を背面に確定し、前面のワイプをリセット(無段差)
      back.style.backgroundImage = `url(${img})`;
      frame.classList.remove('is-revealing');
      current = index;
      busy = false;
    };

    if (reduce) { finish(); return; }

    // 強制リフローでアニメを確実に発火
    void front.offsetWidth;
    frame.classList.add('is-revealing');
    front.addEventListener('transitionend', finish, { once: true });
  };

  document.querySelector('.mr-thumbs')?.addEventListener('click', (e) => {
    const thumb = e.target.closest('.mr-thumb');
    if (thumb) show(thumbs.indexOf(thumb));
  });
})();

コード

HTML
<!-- クリップパスリビール: 画像を斜めのワイプで切替えるスライド遷移 -->
<div class="cr-stage">
  <div class="cr-frame">
    <!-- 2枚を重ね、上のレイヤーを clip-path でワイプ -->
    <div class="cr-layer cr-layer--back"></div>
    <div class="cr-layer cr-layer--front"></div>

    <div class="cr-caption">
      <span class="cr-index">01 / 04</span>
      <h2 class="cr-name">Indigo</h2>
    </div>
  </div>

  <div class="cr-thumbs" role="tablist" aria-label="画像選択">
    <button class="cr-thumb is-on" data-g="linear-gradient(135deg,#5b6cff,#23d5ab)" data-name="Indigo" aria-label="Indigo"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#ff5f6d,#ffc371)" data-name="Coral" aria-label="Coral"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#11998e,#38ef7d)" data-name="Jade" aria-label="Jade"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#c471f5,#fa71cd)" data-name="Orchid" aria-label="Orchid"></button>
  </div>
</div>
CSS
* { box-sizing: border-box; }

:root {
  --text: #f4f6ff;
  --ease: cubic-bezier(.76, 0, .24, 1);
}

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
  background: #07080f;
}

.cr-stage { width: min(640px, 92vw); }

.cr-frame {
  position: relative;
  height: 250px;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 22px 55px rgba(0,0,0,.55);
}

.cr-layer {
  position: absolute;
  inset: 0;
  background-image: var(--g, linear-gradient(135deg,#5b6cff,#23d5ab));
}
/* 前面レイヤーを斜めワイプ。閉=隠れた状態、開=全面 */
.cr-layer--front {
  clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
  transition: clip-path .62s var(--ease);
}
.cr-frame.is-revealing .cr-layer--front {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

.cr-caption {
  position: absolute;
  left: 26px;
  bottom: 22px;
  z-index: 2;
  text-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.cr-index { font-size: 12px; letter-spacing: .25em; opacity: .85; }
.cr-name { margin: 4px 0 0; font-size: 30px; }

.cr-thumbs {
  display: flex;
  gap: 12px;
  margin-top: 16px;
  justify-content: center;
}
.cr-thumb {
  width: 56px;
  height: 40px;
  border-radius: 10px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  background-image: var(--g);
  background-size: cover;
  opacity: .55;
  transition: opacity .25s ease, transform .25s ease, border-color .25s ease;
}
.cr-thumbs .cr-thumb:nth-child(1) { --g: linear-gradient(135deg,#5b6cff,#23d5ab); }
.cr-thumbs .cr-thumb:nth-child(2) { --g: linear-gradient(135deg,#ff5f6d,#ffc371); }
.cr-thumbs .cr-thumb:nth-child(3) { --g: linear-gradient(135deg,#11998e,#38ef7d); }
.cr-thumbs .cr-thumb:nth-child(4) { --g: linear-gradient(135deg,#c471f5,#fa71cd); }
.cr-thumb:hover { opacity: .85; transform: translateY(-2px); }
.cr-thumb.is-on { opacity: 1; border-color: #fff; transform: translateY(-2px); }
.cr-thumb:focus-visible { outline: 2px solid #6c8cff; outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .cr-layer--front { transition-duration: 1ms; }
}

@media (max-width: 520px) {
  .cr-name { font-size: 24px; }
}
JavaScript
// クリップパスリビール: 前面に次画像を載せワイプ→完了後に背面へ確定
(() => {
  const frame = document.querySelector('.cr-frame');
  const back = document.querySelector('.cr-layer--back');
  const front = document.querySelector('.cr-layer--front');
  const nameEl = document.querySelector('.cr-name');
  const indexEl = document.querySelector('.cr-index');
  const thumbs = [...document.querySelectorAll('.cr-thumb')];
  if (!frame || !back || !front || thumbs.length === 0) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let current = 0;
  let busy = false;

  const total = String(thumbs.length).padStart(2, '0');

  const show = (index) => {
    if (busy || index === current) return;
    const thumb = thumbs[index];
    if (!thumb) return;
    busy = true;

    const grad = thumb.dataset.g;
    const name = thumb.dataset.name ?? '';

    // 前面に次の画像を仕込み、ワイプで露出
    front.style.backgroundImage = grad;
    nameEl.textContent = name;
    indexEl.textContent = `${String(index + 1).padStart(2, '0')} / ${total}`;

    thumbs.forEach((t, i) => t.classList.toggle('is-on', i === index));

    const finish = () => {
      // 露出した画像を背面に確定し、前面のワイプをリセット(無段差)
      back.style.backgroundImage = grad;
      frame.classList.remove('is-revealing');
      current = index;
      busy = false;
    };

    if (reduce) {
      finish();
      return;
    }

    // 強制リフローでアニメ確実に発火
    void front.offsetWidth;
    frame.classList.add('is-revealing');
    front.addEventListener('transitionend', finish, { once: true });
  };

  document.querySelector('.cr-thumbs')?.addEventListener('click', (e) => {
    const thumb = e.target.closest('.cr-thumb');
    if (thumb) show(thumbs.indexOf(thumb));
  });
})();

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

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

# 追加してほしい効果
クリップパス画像リビール(ページ遷移 / View Transitions)
前面に次の画像を載せ clip-path の斜めワイプで切替えるギャラリー遷移。完了後に背面へ確定する二層構成で、無段差にスライドし続けます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- クリップパスリビール: 画像を斜めのワイプで切替えるスライド遷移 -->
<div class="cr-stage">
  <div class="cr-frame">
    <!-- 2枚を重ね、上のレイヤーを clip-path でワイプ -->
    <div class="cr-layer cr-layer--back"></div>
    <div class="cr-layer cr-layer--front"></div>

    <div class="cr-caption">
      <span class="cr-index">01 / 04</span>
      <h2 class="cr-name">Indigo</h2>
    </div>
  </div>

  <div class="cr-thumbs" role="tablist" aria-label="画像選択">
    <button class="cr-thumb is-on" data-g="linear-gradient(135deg,#5b6cff,#23d5ab)" data-name="Indigo" aria-label="Indigo"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#ff5f6d,#ffc371)" data-name="Coral" aria-label="Coral"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#11998e,#38ef7d)" data-name="Jade" aria-label="Jade"></button>
    <button class="cr-thumb" data-g="linear-gradient(135deg,#c471f5,#fa71cd)" data-name="Orchid" aria-label="Orchid"></button>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }

:root {
  --text: #f4f6ff;
  --ease: cubic-bezier(.76, 0, .24, 1);
}

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
  background: #07080f;
}

.cr-stage { width: min(640px, 92vw); }

.cr-frame {
  position: relative;
  height: 250px;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 22px 55px rgba(0,0,0,.55);
}

.cr-layer {
  position: absolute;
  inset: 0;
  background-image: var(--g, linear-gradient(135deg,#5b6cff,#23d5ab));
}
/* 前面レイヤーを斜めワイプ。閉=隠れた状態、開=全面 */
.cr-layer--front {
  clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
  transition: clip-path .62s var(--ease);
}
.cr-frame.is-revealing .cr-layer--front {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

.cr-caption {
  position: absolute;
  left: 26px;
  bottom: 22px;
  z-index: 2;
  text-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.cr-index { font-size: 12px; letter-spacing: .25em; opacity: .85; }
.cr-name { margin: 4px 0 0; font-size: 30px; }

.cr-thumbs {
  display: flex;
  gap: 12px;
  margin-top: 16px;
  justify-content: center;
}
.cr-thumb {
  width: 56px;
  height: 40px;
  border-radius: 10px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  background-image: var(--g);
  background-size: cover;
  opacity: .55;
  transition: opacity .25s ease, transform .25s ease, border-color .25s ease;
}
.cr-thumbs .cr-thumb:nth-child(1) { --g: linear-gradient(135deg,#5b6cff,#23d5ab); }
.cr-thumbs .cr-thumb:nth-child(2) { --g: linear-gradient(135deg,#ff5f6d,#ffc371); }
.cr-thumbs .cr-thumb:nth-child(3) { --g: linear-gradient(135deg,#11998e,#38ef7d); }
.cr-thumbs .cr-thumb:nth-child(4) { --g: linear-gradient(135deg,#c471f5,#fa71cd); }
.cr-thumb:hover { opacity: .85; transform: translateY(-2px); }
.cr-thumb.is-on { opacity: 1; border-color: #fff; transform: translateY(-2px); }
.cr-thumb:focus-visible { outline: 2px solid #6c8cff; outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .cr-layer--front { transition-duration: 1ms; }
}

@media (max-width: 520px) {
  .cr-name { font-size: 24px; }
}

【JavaScript】
// クリップパスリビール: 前面に次画像を載せワイプ→完了後に背面へ確定
(() => {
  const frame = document.querySelector('.cr-frame');
  const back = document.querySelector('.cr-layer--back');
  const front = document.querySelector('.cr-layer--front');
  const nameEl = document.querySelector('.cr-name');
  const indexEl = document.querySelector('.cr-index');
  const thumbs = [...document.querySelectorAll('.cr-thumb')];
  if (!frame || !back || !front || thumbs.length === 0) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let current = 0;
  let busy = false;

  const total = String(thumbs.length).padStart(2, '0');

  const show = (index) => {
    if (busy || index === current) return;
    const thumb = thumbs[index];
    if (!thumb) return;
    busy = true;

    const grad = thumb.dataset.g;
    const name = thumb.dataset.name ?? '';

    // 前面に次の画像を仕込み、ワイプで露出
    front.style.backgroundImage = grad;
    nameEl.textContent = name;
    indexEl.textContent = `${String(index + 1).padStart(2, '0')} / ${total}`;

    thumbs.forEach((t, i) => t.classList.toggle('is-on', i === index));

    const finish = () => {
      // 露出した画像を背面に確定し、前面のワイプをリセット(無段差)
      back.style.backgroundImage = grad;
      frame.classList.remove('is-revealing');
      current = index;
      busy = false;
    };

    if (reduce) {
      finish();
      return;
    }

    // 強制リフローでアニメ確実に発火
    void front.offsetWidth;
    frame.classList.add('is-revealing');
    front.addEventListener('transitionend', finish, { once: true });
  };

  document.querySelector('.cr-thumbs')?.addEventListener('click', (e) => {
    const thumb = e.target.closest('.cr-thumb');
    if (thumb) show(thumbs.indexOf(thumb));
  });
})();

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

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