CSSのclip-pathで、画像を好きな形に切り抜く方法
img 要素に clip-path を1行足すと、画像を円や三角形、六角形の形に切り抜けます。画像編集ソフトで切り抜いた画像を用意しなくても、元の四角い画像をそのまま使えます。形は circle()、ellipse()、polygon()、inset() の4つの関数で指定します。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、getComputedStyle() の値と、スクリーンショットの画素の色を読んで、切り抜かれた形を確かめました。切り抜く画像には、筆者が描いた SVG の絵(空と丘と猫)を使っています。Firefox と Safari、スマートフォンの実機での表示は試していません。

形ごとの書き方
clip-path は、要素のどの部分を表示するかを決めるプロパティです。指定した形の内側だけが表示され、外側は表示されなくなります。検証では、次の CSS を使いました。
/* 切り抜く画像。大きさをそろえ、object-fit で枠いっぱいに表示する */
.photo {
display: block;
width: 200px;
height: 200px;
object-fit: cover;
}
/* 円 */
.circle {
clip-path: circle(50%);
}
/* 楕円(横の半径 50%、縦の半径 35%) */
.ellipse {
clip-path: ellipse(50% 35%);
}
/* 三角形(上の真ん中、右下、左下) */
.triangle {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
/* 六角形 */
.hexagon {
clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}
/* 四辺を 10% ずつ内側に入れて、角を丸める */
.inset {
clip-path: inset(10% round 24px);
}
<img class="photo circle" src="photo.svg" alt="">
元の画像は 300×200 の横長ですが、object-fit: cover で 200×200 の枠いっぱいに表示してから切り抜いています。
200×200 の画像で、切り抜いた後に画像が見えている範囲を、行ごとに数えました。ページの背景の白ではない画素が、その行に何 px あったかを数えた値です。
| 形 | いちばん上の行(y=0) | 真ん中の行(y=100) | 画像が見える行の範囲 |
|---|---|---|---|
| 切り抜きなし | 200px | 200px | 0〜199 |
circle(50%) |
26px | 200px | 0〜199 |
ellipse(50% 35%) |
0px | 200px | 30〜169 |
polygon() の三角形 |
2px | 102px | 0〜199 |
polygon() の六角形 |
102px | 200px | 0〜199 |
inset(10% round 24px) |
0px | 160px | 20〜179 |
circle(50%) は、真ん中の行で幅いっぱいの 200px、上下の端では 26px まで細くなり、半径 100px の円になっています。ellipse(50% 35%) は横の半径が幅の 50% で 100px、縦の半径が高さの 35% で 70px です。画像が見えるのは y=30〜169 の 140px だけでした。
polygon() には、頂点の座標を「横 縦」の組で、カンマで区切って並べます。% は要素の幅と高さに対する割合です。三角形は、頂点のある上の行では 2px、真ん中の行では 102px、下の行では 200px と、下へ行くほど広がりました。六角形は、上の辺の 25%〜75% にあたる x=49〜150 から始まり、真ん中の行で幅いっぱいになります。
inset() は、上・右・下・左の各辺から、指定した分だけ内側に入った四角形を作ります。10% は 200px の 10% の 20px なので、画像が見えるのは x=20〜179、y=20〜179 の範囲です。round 24px で角も丸まり、上の辺の上でも、角に近い (30, 20) の画素は白、角から離れた (50, 20) の画素は画像の色でした。
横長の要素に circle(50%) を付けると、円の上下が欠ける
circle() の半径を % で書くと、要素の幅や高さそのものではなく、sqrt(幅² + 高さ²) / sqrt(2) に対する割合になります。MDN の basic-shape の説明にある計算です。正方形なら1辺の長さと同じですが、横長の要素では短い辺より大きくなります。
300×200 の要素で計算すると、sqrt(300² + 200²) / sqrt(2) は約 255px で、circle(50%) の半径は約 127.5px です。要素の高さの半分の 100px より大きいので、円は上下にはみ出し、上下の端は要素の範囲で切れます。

検証環境で、2つの書き方を比べました。
| 書き方 | いちばん上の行(y=0) | 真ん中の行(y=100) |
|---|---|---|
circle(50%) |
162px | 256px |
circle() |
26px | 200px |
circle(50%) では、真ん中の行の幅が 256px で、計算どおり半径が約 127.5px の円になっていました。いちばん上の行でも 162px が見えていて、円の上の部分が要素の範囲で切り落とされています。
半径を省くと、半径は closest-side、つまり円の中心から要素のいちばん近い辺までの長さになります。300×200 の要素では、上下の辺までの 100px です。circle() では真ん中の行が 200px、いちばん上の行が 26px で、正方形のときの circle(50%) と同じ形の正円になりました。横長や縦長の画像を正円に切り抜くときは、circle() と書くか、circle(100px) のように半径を長さで書きます。
border も box-shadow も、一緒に切り抜かれる
clip-path の形の基準になる箱(参照ボックス)は、指定しなければ border-box、つまり border の外側の端で囲んだ箱です。MDN の clip-path の説明にそう書かれています。border の外に広がる box-shadow は、形の外になるので表示されません。

200×200 の画像に border: 6px solid と、外側に 6px 広がる box-shadow を付け、circle(50%) で切り抜いて確かめました。要素は border を含めて 212×212 になり、円の半径は 106px になります。
| 調べた位置 | 色 |
|---|---|
| border のすぐ外の左・上・右(box-shadow がある場所) | 3か所とも白 |
| 上下左右の辺の真ん中の border | 4か所とも border のピンク |
| 左上と右下の角の border | 白 |
box-shadow は、どこも表示されませんでした。border は、円が要素の辺に接する上下左右の4か所にだけ残り、角の部分は切り抜かれています。border を付けても、円い枠にはなりません。
影を付けるときは、親要素に drop-shadow を付ける
六角形に切り抜いた画像に box-shadow: 0 6px 4px rgb(0 0 0 / 0.5) を付けても、影は見えませんでした。影が切り抜かれるのは、前の節と同じ理由です。
影を付けたいときは、画像を要素で包み、その要素に filter: drop-shadow() を付けます。
<div class="shadow-wrap">
<img class="photo hexagon" src="photo.svg" alt="">
</div>
.shadow-wrap {
filter: drop-shadow(0 6px 4px rgb(0 0 0 / 0.5));
width: 200px;
}
六角形の下の辺の真ん中から、下へ画素の色を読んだ結果です。
| 六角形の下の端から | 画像に box-shadow |
親要素に drop-shadow |
|---|---|---|
| 2px 下 | rgb(255, 255, 255) |
rgb(154, 154, 154) |
| 5px 下 | rgb(255, 255, 255) |
rgb(185, 185, 185) |
| 8px 下 | rgb(255, 255, 255) |
rgb(219, 219, 219) |
drop-shadow では、下へ行くほど薄くなる灰色の影が出ました。左下の斜めの辺の外側(要素の左の辺から 12px、上の辺から 175px の位置)は、どちらの書き方でも白でした。影が四角形ではなく、六角形の形に沿って付いています。MDN の drop-shadow() の説明にも、box-shadow は要素のボックス全体の後ろに長方形の影を作り、drop-shadow() は画像の形(アルファチャネル)に合った影を作る、とあります。
切り抜いた外側をクリックすると、下にある要素に届く
clip-path で見えなくなった部分は、クリックにも反応しませんでした。circle(50%) で切り抜いた 200×200 の画像の、次の4か所を Playwright でクリックし、document.elementFromPoint() でその位置にある要素も調べました。円の中心は (100, 100)、半径は 100px です。
| クリックした位置 | 画像に clip-path を付けた場合 |
|---|---|
| 角 (8, 8) | 親の div が受け取る |
| 円の縁のすぐ外 (28, 28) | 親の div が受け取る |
| 円の縁のすぐ内 (32, 32) | img が受け取る |
| 中心 (100, 100) | img が受け取る |
(28, 28) は円の中心から約 101.8px、(32, 32) は約 96.2px の位置です。円の外では、クリックは画像を通り抜けて、後ろにある親の div に届きました。
気をつけたいのは、画像をリンクで包むときです。<a href="…"><img class="photo circle" …></a> のように画像にだけ clip-path を付けると、角 (8, 8) や円のすぐ外 (28, 28) では a 要素自体がクリックを受け取り、リンク先へ移動しました。切り抜かれたのは画像だけで、a 要素の四角い範囲は残っているからです。
<a class="circle" href="#ok2" style="display: block; width: 200px;">
<img class="photo" src="photo.svg" alt="リンク2">
</a>
a 要素のほうに clip-path を付けると、角と円のすぐ外はリンクの後ろの要素に届き、リンク先へは移動しませんでした。円の内側の2か所では、リンク先へ移動しました。丸いボタンのように、見えている形だけを押せるようにしたいときは、クリックを受け取る要素そのものを切り抜きます。
形を変えるアニメーションは、頂点の数をそろえる
clip-path の形は、アニメーションで少しずつ変えられます。ただし、MDN の basic-shape の説明によると、途中の形が計算されるのは、2つの形が同じ種類の関数で、値の数と種類がそろっているときだけです。polygon() どうしなら、頂点の数が同じでなければなりません。そろっていないときは、途中の形を作らずに切り替わります。
検証では、clip-path を変える 1 秒のアニメーションを Web Animations API の element.animate() で作り、途中で止めて、getComputedStyle() の値を読みました。
| 変化 | 0.25 秒 | 0.5 秒 | 0.75 秒 |
|---|---|---|---|
| 三角形(3点)→ 六角形(6点) | 三角形のまま | 六角形 | 六角形 |
| 頂点を6つにした三角形 → 六角形 | 途中の形 | 途中の形 | 途中の形 |
circle(30%) → circle(50%) |
circle(35%) |
circle(40%) |
circle(45%) |
circle(50%) → 六角形の polygon() |
circle(50%) |
六角形 | 六角形 |
頂点の数が違う三角形と六角形の間では、0.5 秒の時点で、いきなり六角形に変わりました。circle() と polygon() のように関数の種類が違うときも同じです。

三角形から六角形へ少しずつ変えたいときは、三角形も6つの頂点で書きます。上の頂点を2つ重ね、左右の辺の真ん中にも頂点を置くと、見た目は三角形のままで、頂点の数が六角形とそろいます。
/* 見た目は三角形。頂点は六角形と同じ6つ */
clip-path: polygon(50% 0, 50% 0, 75% 50%, 100% 100%, 0 100%, 25% 50%);
この三角形から六角形への変化では、0.5 秒の時点で polygon(37.5% 0px, 62.5% 0px, 87.5% 50%, 87.5% 100%, 12.5% 100%, 12.5% 50%) と、どの頂点も始まりと終わりのちょうど中間の位置にありました。重ねていた上の2つの頂点は左右に分かれ、1 秒で六角形の上の辺になります。





