クリップパス円ワイプ
clip-path: circle() を補間し、クリック地点を起点に円が広がってパネルを切り替えるトランジション。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:MVギャラリーの円ワイプ切り替え -->
<section class="cw-stage">
<header class="cw-head">
<div class="cw-brand">🌸 Sakura</div>
<p class="cw-label">MUSIC VIDEO</p>
</header>
<!-- パネル:クリック地点を起点に円が広がって次のMVへ -->
<div class="cw-screen" id="cwScreen">
<div class="cw-panel cw-panel--base" id="cwBase"></div>
<div class="cw-panel cw-panel--next" id="cwNext"></div>
<div class="cw-meta" id="cwMeta">
<span class="cw-tag" id="cwTag">01 / 03</span>
<h2 class="cw-title" id="cwTitle">春一番デイズ</h2>
<p class="cw-credit" id="cwCredit">2025 SPRING SINGLE</p>
</div>
<span class="cw-play">▶</span>
</div>
<div class="cw-dots" id="cwDots">
<button class="cw-dot is-active" type="button" aria-label="MV 1"></button>
<button class="cw-dot" type="button" aria-label="MV 2"></button>
<button class="cw-dot" type="button" aria-label="MV 3"></button>
</div>
<p class="cw-hint">パネルをクリックで次のMVへ(クリック地点から円ワイプ)</p>
</section>
CSS
/* Sakura:MVの円ワイプトランジション */
:root {
--pink: #ffd1e0;
--hot: #ff6fa5;
--gray: #f2f3f5;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic UI", system-ui, sans-serif;
background: linear-gradient(180deg, #fff6fa 0%, var(--pink) 100%);
color: #5a3b48;
}
.cw-stage { height: 400px; padding: 14px 20px; display: flex; flex-direction: column; }
.cw-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.cw-brand { font-size: 15px; font-weight: 800; letter-spacing: 0.04em; color: var(--hot); }
.cw-label { margin: 0; font-size: 10px; letter-spacing: 0.18em; color: #b89aa6; }
/* スクリーン */
.cw-screen {
position: relative;
flex: 1;
border-radius: 18px;
overflow: hidden;
cursor: pointer;
box-shadow: 0 18px 40px -18px rgba(214, 86, 132, 0.6);
}
/* MVパネル(base=現在、next=これから出る側) */
.cw-panel { position: absolute; inset: 0; background-size: cover; background-position: center; }
.cw-panel--next {
/* 初期は中心の半径0の円 → JSで clip-path を広げる */
clip-path: circle(0% at 50% 50%);
}
/* 上に乗るテキスト */
.cw-meta {
position: absolute;
left: 18px; bottom: 16px; right: 18px;
text-shadow: 0 2px 12px rgba(60, 20, 40, 0.6);
color: #fff;
pointer-events: none;
transition: opacity 0.3s ease;
}
.cw-tag {
display: inline-block;
font-size: 10px; letter-spacing: 0.1em; font-weight: 700;
padding: 3px 10px; border-radius: 999px;
background: rgba(255, 111, 165, 0.85);
margin-bottom: 8px;
}
.cw-title { margin: 0 0 4px; font-size: 24px; font-weight: 800; }
.cw-credit { margin: 0; font-size: 11px; letter-spacing: 0.08em; opacity: 0.85; }
.cw-play {
position: absolute;
top: 50%; left: 50%;
width: 50px; height: 50px;
margin: -25px 0 0 -25px;
display: grid; place-items: center;
font-size: 18px; color: var(--hot);
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 8px 20px rgba(214, 86, 132, 0.5);
pointer-events: none;
}
.cw-dots { display: flex; gap: 7px; justify-content: center; margin: 10px 0 4px; }
.cw-dot {
width: 8px; height: 8px; padding: 0;
border: none; border-radius: 50%; cursor: pointer;
background: rgba(255, 111, 165, 0.3);
transition: background 0.2s ease, transform 0.2s ease;
}
.cw-dot.is-active { background: var(--hot); transform: scale(1.3); }
.cw-hint { margin: 2px 0 0; font-size: 10px; text-align: center; color: #b89aa6; }
@media (prefers-reduced-motion: reduce) {
.cw-panel--next { transition: none; }
}
JavaScript
// Sakura:クリック地点を起点に clip-path 円を広げてMVを切り替える
(() => {
const screen = document.getElementById("cwScreen");
const base = document.getElementById("cwBase");
const next = document.getElementById("cwNext");
const tag = document.getElementById("cwTag");
const title = document.getElementById("cwTitle");
const credit = document.getElementById("cwCredit");
const dotsWrap = document.getElementById("cwDots");
if (!screen || !base || !next) return; // null安全
// MVデータ(実在名NG・架空名)
const mvs = [
{ img: 71, title: "春一番デイズ", credit: "2025 SPRING SINGLE" },
{ img: 72, title: "ヒラリ、サクラ", credit: "2024 5th SINGLE" },
{ img: 73, title: "ムーンライト・ステップ", credit: "2024 ALBUM「夜明け」" },
];
let cur = 0;
let busy = false;
const bg = (n) => `url("https://picsum.photos/640/280?random=${n}")`;
const dots = dotsWrap ? Array.from(dotsWrap.querySelectorAll(".cw-dot")) : [];
// 初期表示
base.style.backgroundImage = bg(mvs[0].img);
// メタ情報を反映
const applyMeta = (i) => {
if (tag) tag.textContent = `0${i + 1} / 0${mvs.length}`;
if (title) title.textContent = mvs[i].title;
if (credit) credit.textContent = mvs[i].credit;
dots.forEach((d, k) => d.classList.toggle("is-active", k === i));
};
// クリック座標(%)を起点に円ワイプ
const wipeTo = (i, ox, oy) => {
if (busy || i === cur) return;
busy = true;
next.style.backgroundImage = bg(mvs[i].img);
// いったん半径0にしてから広げる
next.style.transition = "none";
next.style.clipPath = `circle(0% at ${ox}% ${oy}%)`;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
next.style.transition = "clip-path 0.75s cubic-bezier(0.65, 0, 0.35, 1)";
// 画面対角を覆う十分大きな半径まで広げる
next.style.clipPath = `circle(150% at ${ox}% ${oy}%)`;
applyMeta(i);
});
});
// 完了したら next を base に昇格させ、next をリセット
const done = () => {
base.style.backgroundImage = bg(mvs[i].img);
next.style.transition = "none";
next.style.clipPath = "circle(0% at 50% 50%)";
cur = i;
busy = false;
next.removeEventListener("transitionend", done);
};
next.addEventListener("transitionend", done);
};
// パネルクリック → 次のMVへ(クリック地点起点)
screen.addEventListener("click", (e) => {
const rect = screen.getBoundingClientRect();
const ox = ((e.clientX - rect.left) / rect.width) * 100;
const oy = ((e.clientY - rect.top) / rect.height) * 100;
wipeTo((cur + 1) % mvs.length, ox.toFixed(1), oy.toFixed(1));
});
// ドット → 中央起点でそのMVへ
dots.forEach((d, i) => {
d.addEventListener("click", (e) => {
e.stopPropagation();
wipeTo(i, 50, 50);
});
});
applyMeta(0);
})();
コード
HTML
<!-- クリップパス円ワイプ:クリック地点から円が広がり次パネルへ切替 -->
<div class="wipe-stage" id="wipeStage">
<!-- パネル群(重ねて上から clip-path で出し入れ) -->
<section class="wipe-panel wipe-p1" data-panel>
<span class="wipe-kicker">PANEL 01</span>
<h2 class="wipe-h">Sunrise</h2>
<p class="wipe-sub">円が広がるトランジション</p>
</section>
<section class="wipe-panel wipe-p2" data-panel>
<span class="wipe-kicker">PANEL 02</span>
<h2 class="wipe-h">Ocean</h2>
<p class="wipe-sub">クリック地点が起点になる</p>
</section>
<section class="wipe-panel wipe-p3" data-panel>
<span class="wipe-kicker">PANEL 03</span>
<h2 class="wipe-h">Aurora</h2>
<p class="wipe-sub">clip-path: circle() を補間</p>
</section>
<button class="wipe-next" id="wipeNext" type="button">次へ ›</button>
<p class="wipe-hint">画面のどこかをクリック / タップ</p>
</div>
CSS
/* パネルを重ね、上層を円形クリップで出し入れする */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
color: #fff;
}
.wipe-stage {
position: relative;
width: 100%;
min-height: 360px;
height: 360px;
overflow: hidden;
cursor: pointer;
user-select: none;
}
/* 各パネルは全面に重なる */
.wipe-panel {
position: absolute; inset: 0;
display: grid;
align-content: center;
justify-items: center;
gap: 6px;
text-align: center;
/* 起点はJSで --x/--y を設定。初期は閉じた円(0)=非表示 */
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
transition: clip-path .7s cubic-bezier(.77, 0, .18, 1);
will-change: clip-path;
}
/* アクティブパネルは完全に開く */
.wipe-panel.is-active {
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
z-index: 2;
}
/* 直前のパネルは下に残し背景として見せる */
.wipe-panel.is-prev { clip-path: circle(150% at 50% 50%); z-index: 1; }
/* 配色(各パネルのテーマ) */
.wipe-p1 { background: linear-gradient(135deg, #ff9a3c 0%, #ff6a88 60%, #ff99ac 100%); }
.wipe-p2 { background: linear-gradient(135deg, #2af598 0%, #009efd 100%); }
.wipe-p3 { background: linear-gradient(135deg, #7028e4 0%, #e5b2ca 60%, #38f9d7 100%); }
.wipe-kicker {
font-size: 11px; font-weight: 800; letter-spacing: .28em;
opacity: .85;
}
.wipe-h {
margin: 0;
font-size: 38px; font-weight: 800; letter-spacing: .01em;
text-shadow: 0 4px 20px rgba(0,0,0,.25);
}
.wipe-sub { margin: 0; font-size: 13px; opacity: .92; }
.wipe-next {
position: absolute; right: 16px; bottom: 16px; z-index: 3;
padding: 9px 16px;
border: 1px solid rgba(255,255,255,.6);
border-radius: 999px;
background: rgba(255,255,255,.18);
color: #fff; font-size: 13px; font-weight: 700;
cursor: pointer;
backdrop-filter: blur(4px);
transition: background .2s ease, transform .1s ease;
}
.wipe-next:hover { background: rgba(255,255,255,.32); }
.wipe-next:active { transform: scale(.96); }
.wipe-hint {
position: absolute; left: 16px; bottom: 18px; z-index: 3; margin: 0;
font-size: 11px; letter-spacing: .06em; opacity: .8;
}
@media (prefers-reduced-motion: reduce) {
.wipe-panel { transition: none; }
}
JavaScript
// クリップパス円ワイプ:クリック位置を起点に円を広げ次パネルへ
(() => {
const stage = document.getElementById('wipeStage');
const nextBtn = document.getElementById('wipeNext');
if (!stage) return; // null安全
const panels = Array.from(stage.querySelectorAll('[data-panel]'));
if (!panels.length) return;
let current = 0;
panels[0].classList.add('is-active'); // 初期表示
// px座標を stage 比率(%)へ変換し、起点としてセット
const goTo = (index, clientX, clientY) => {
const next = (index + panels.length) % panels.length;
if (next === current) return;
const rect = stage.getBoundingClientRect();
// 座標未指定なら中央から
const px = clientX == null ? rect.left + rect.width / 2 : clientX;
const py = clientY == null ? rect.top + rect.height / 2 : clientY;
const x = ((px - rect.left) / rect.width) * 100;
const y = ((py - rect.top) / rect.height) * 100;
const cur = panels[current];
const nxt = panels[next];
// 起点を設定してから開く
nxt.style.setProperty('--x', x.toFixed(1) + '%');
nxt.style.setProperty('--y', y.toFixed(1) + '%');
panels.forEach((p) => p.classList.remove('is-prev'));
cur.classList.remove('is-active');
cur.classList.add('is-prev'); // 下敷きとして残す
nxt.classList.add('is-active');
current = next;
};
// ステージクリックで次へ(ボタンは除外)
stage.addEventListener('click', (e) => {
if (e.target.closest('.wipe-next')) return;
goTo(current + 1, e.clientX, e.clientY);
});
// 「次へ」ボタン:ボタン中心を起点に
if (nextBtn) {
nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
const r = nextBtn.getBoundingClientRect();
goTo(current + 1, r.left + r.width / 2, r.top + r.height / 2);
});
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「クリップパス円ワイプ」の効果を追加してください。
# 追加してほしい効果
クリップパス円ワイプ(アニメーション & トランジション)
clip-path: circle() を補間し、クリック地点を起点に円が広がってパネルを切り替えるトランジション。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- クリップパス円ワイプ:クリック地点から円が広がり次パネルへ切替 -->
<div class="wipe-stage" id="wipeStage">
<!-- パネル群(重ねて上から clip-path で出し入れ) -->
<section class="wipe-panel wipe-p1" data-panel>
<span class="wipe-kicker">PANEL 01</span>
<h2 class="wipe-h">Sunrise</h2>
<p class="wipe-sub">円が広がるトランジション</p>
</section>
<section class="wipe-panel wipe-p2" data-panel>
<span class="wipe-kicker">PANEL 02</span>
<h2 class="wipe-h">Ocean</h2>
<p class="wipe-sub">クリック地点が起点になる</p>
</section>
<section class="wipe-panel wipe-p3" data-panel>
<span class="wipe-kicker">PANEL 03</span>
<h2 class="wipe-h">Aurora</h2>
<p class="wipe-sub">clip-path: circle() を補間</p>
</section>
<button class="wipe-next" id="wipeNext" type="button">次へ ›</button>
<p class="wipe-hint">画面のどこかをクリック / タップ</p>
</div>
【CSS】
/* パネルを重ね、上層を円形クリップで出し入れする */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
color: #fff;
}
.wipe-stage {
position: relative;
width: 100%;
min-height: 360px;
height: 360px;
overflow: hidden;
cursor: pointer;
user-select: none;
}
/* 各パネルは全面に重なる */
.wipe-panel {
position: absolute; inset: 0;
display: grid;
align-content: center;
justify-items: center;
gap: 6px;
text-align: center;
/* 起点はJSで --x/--y を設定。初期は閉じた円(0)=非表示 */
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
transition: clip-path .7s cubic-bezier(.77, 0, .18, 1);
will-change: clip-path;
}
/* アクティブパネルは完全に開く */
.wipe-panel.is-active {
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
z-index: 2;
}
/* 直前のパネルは下に残し背景として見せる */
.wipe-panel.is-prev { clip-path: circle(150% at 50% 50%); z-index: 1; }
/* 配色(各パネルのテーマ) */
.wipe-p1 { background: linear-gradient(135deg, #ff9a3c 0%, #ff6a88 60%, #ff99ac 100%); }
.wipe-p2 { background: linear-gradient(135deg, #2af598 0%, #009efd 100%); }
.wipe-p3 { background: linear-gradient(135deg, #7028e4 0%, #e5b2ca 60%, #38f9d7 100%); }
.wipe-kicker {
font-size: 11px; font-weight: 800; letter-spacing: .28em;
opacity: .85;
}
.wipe-h {
margin: 0;
font-size: 38px; font-weight: 800; letter-spacing: .01em;
text-shadow: 0 4px 20px rgba(0,0,0,.25);
}
.wipe-sub { margin: 0; font-size: 13px; opacity: .92; }
.wipe-next {
position: absolute; right: 16px; bottom: 16px; z-index: 3;
padding: 9px 16px;
border: 1px solid rgba(255,255,255,.6);
border-radius: 999px;
background: rgba(255,255,255,.18);
color: #fff; font-size: 13px; font-weight: 700;
cursor: pointer;
backdrop-filter: blur(4px);
transition: background .2s ease, transform .1s ease;
}
.wipe-next:hover { background: rgba(255,255,255,.32); }
.wipe-next:active { transform: scale(.96); }
.wipe-hint {
position: absolute; left: 16px; bottom: 18px; z-index: 3; margin: 0;
font-size: 11px; letter-spacing: .06em; opacity: .8;
}
@media (prefers-reduced-motion: reduce) {
.wipe-panel { transition: none; }
}
【JavaScript】
// クリップパス円ワイプ:クリック位置を起点に円を広げ次パネルへ
(() => {
const stage = document.getElementById('wipeStage');
const nextBtn = document.getElementById('wipeNext');
if (!stage) return; // null安全
const panels = Array.from(stage.querySelectorAll('[data-panel]'));
if (!panels.length) return;
let current = 0;
panels[0].classList.add('is-active'); // 初期表示
// px座標を stage 比率(%)へ変換し、起点としてセット
const goTo = (index, clientX, clientY) => {
const next = (index + panels.length) % panels.length;
if (next === current) return;
const rect = stage.getBoundingClientRect();
// 座標未指定なら中央から
const px = clientX == null ? rect.left + rect.width / 2 : clientX;
const py = clientY == null ? rect.top + rect.height / 2 : clientY;
const x = ((px - rect.left) / rect.width) * 100;
const y = ((py - rect.top) / rect.height) * 100;
const cur = panels[current];
const nxt = panels[next];
// 起点を設定してから開く
nxt.style.setProperty('--x', x.toFixed(1) + '%');
nxt.style.setProperty('--y', y.toFixed(1) + '%');
panels.forEach((p) => p.classList.remove('is-prev'));
cur.classList.remove('is-active');
cur.classList.add('is-prev'); // 下敷きとして残す
nxt.classList.add('is-active');
current = next;
};
// ステージクリックで次へ(ボタンは除外)
stage.addEventListener('click', (e) => {
if (e.target.closest('.wipe-next')) return;
goTo(current + 1, e.clientX, e.clientY);
});
// 「次へ」ボタン:ボタン中心を起点に
if (nextBtn) {
nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
const r = nextBtn.getBoundingClientRect();
goTo(current + 1, r.left + r.width / 2, r.top + r.height / 2);
});
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。