クリップパス画像リビール
前面に次の画像を載せ clip-path の斜めワイプで切替えるギャラリー遷移。完了後に背面へ確定する二層構成で、無段差にスライドし続けます。
ライブデモ
使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。