CSSのclip-pathで、画像を好きな形に切り抜く方法

​​CSS​HTML,Playwright

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、スマートフォンの実機での表示は試していません。

3行2列に並べた6つの画像。1行目は、切り抜きなしの四角い画像と、円に切り抜いた画像。2行目は、横長の楕円と三角形に切り抜いた画像。3行目は、六角形と、角を丸めた少し小さい四角に切り抜いた画像。どれも空と丘と猫の同じ絵
同じ画像に、この記事の CSS の clip-path をそれぞれ付けて Chromium で撮ったもの(下の文字は画像用に足したもの)

形ごとの書き方

clip-path は、要素のどの部分を表示するかを決めるプロパティです。指定した形の内側だけが表示され、外側は表示されなくなります。検証では、次の CSS を使いました。

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);
}
html
<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つの画像を並べたもの。どちらも 300×200 の横長の要素で、要素の範囲を灰色の点線で囲んでいる。上の circle(50%) は、左右は丸いが、上下は点線の辺でまっすぐに切れている。下の circle() は、点線の上下の辺に接する正円になっている
300×200 の画像に circle(50%) と circle() を付けて Chromium で撮ったもの(点線と上の文字は画像用に足したもの)

検証環境で、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 は、形の外になるので表示されません。

横に3つ並べた画像。左は、円に切り抜いた画像に、ピンクの border と濃い水色の box-shadow を付けたもの。濃い水色は見えず、ピンクは円の上下左右の4か所にだけ細く見える。真ん中は、六角形に切り抜いた画像に box-shadow を付けたもので、影は見えない。右は、六角形に切り抜いた画像の親要素に filter: drop-shadow を付けたもので、六角形の形に沿った影が下に見える
border と box-shadow、六角形への box-shadow、親要素への drop-shadow を、それぞれ Chromium で撮ったもの(下の文字は画像用に足したもの)

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() を付けます。

html
<div class="shadow-wrap">
  <img class="photo hexagon" src="photo.svg" alt="">
</div>
css
.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 要素の四角い範囲は残っているからです。

html
<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() のように関数の種類が違うときも同じです。

上下2段に、それぞれ 0 秒、0.25 秒、0.5 秒、0.75 秒、1 秒の時点の形を5つずつ並べたもの。上の段は頂点の数が違う場合で、0.25 秒までは三角形、0.5 秒からは六角形になっている。下の段は頂点の数をそろえた場合で、三角形の上の頂点が左右に分かれ、少しずつ六角形に近づいていく
三角形から六角形へ変える 1 秒のアニメーションを、各時点で止めて Chromium で撮ったもの(見出しと下の文字は画像用に足したもの)

三角形から六角形へ少しずつ変えたいときは、三角形も6つの頂点で書きます。上の頂点を2つ重ね、左右の辺の真ん中にも頂点を置くと、見た目は三角形のままで、頂点の数が六角形とそろいます。

css
/* 見た目は三角形。頂点は六角形と同じ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 秒で六角形の上の辺になります。

​Posted by 86