CSSのshape()関数で、曲線を含む形に切り抜く方法

​​CSS​HTML,Playwright

clip-path に shape() 関数を書くと、波やアーチのような曲線を含む形に要素を切り抜けます。座標は % や calc() で書けるので、要素の幅が変わっても形が一緒に伸び縮みします。SVG のパスの文字列を書く path() では、座標が px に固定されるため、これができません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、スクリーンショットの画素と getComputedStyle() の値で形を確かめました。Firefox と Safari、スマートフォンの実機での表示は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

shape() の書き方

shape() には、最初に from で書き始めの点を書き、そのあとに線を引く命令をカンマで区切って並べます。座標は「横 縦」の順で、要素の左上が 0 0 です。% は要素の幅と高さに対する割合になります。

css
.triangle {
  clip-path: shape(from 0 0, line to 100% 0, line to 50% 100%, close);
}

この例では、左上から右上へ線を引き、下の辺の真ん中へ線を引いてから、close で書き始めの点へ戻ります。上の辺の両端と、下の辺の真ん中を結んだ三角形に切り抜かれます。shape() は clip-path のほか、要素を線に沿って動かす offset-path にも書けます。

使える命令は次のとおりです。どの命令も、前の命令が終わった点から始まります。

命令 引く線
line to x y その点まで直線
hline to x、vline to y 横だけ、縦だけの直線
curve to x y with 制御点 ベジェ曲線
smooth to x y 前の曲線になめらかにつながるベジェ曲線
arc to x y of 半径 円や楕円の一部(円弧)
move to x y 線を引かずに点を動かす
close 書き始めの点まで直線で戻る

to を by に変えると、終わりの点を「今いる点からどれだけ動くか」で書けます。命令のあいだのカンマは省けません。検証環境で、from 0 0 のあとのカンマを抜いた値を CSS.supports() で調べると、false が返りました。

curve と smooth で、下の辺を波にする

見出しの下の辺を波の形にしてみます。

css
.hero {
  clip-path: shape(
    from 0 0,
    hline to 100%,
    vline to calc(100% - 30px),
    curve to 50% calc(100% - 30px) with 75% 100%,
    smooth to 0 calc(100% - 30px),
    close
  );
}
上は「お知らせ」と書かれた横長の水色の帯で、下の辺が右から左へ、谷と山の1回ずつの波になっている。下は「入口」と書かれた縦長の水色の枠で、上の辺が半円のアーチになっている
この記事の CSS の波とアーチを Chromium で表示したもの。アーチは幅 160px・高さ 220px、半径 80px で撮った

左上から右上へ横線を引き、右の辺を下から 30px のところまで下ろします。そこから curve で、下の辺の真ん中まで曲線を引きます。with のあとの 75% 100% は、曲線の形を決める制御点です。曲線は制御点を通りませんが、制御点のほうへ引っぱられて、下にふくらみます。

with のあとに制御点を1つ書くと2次ベジェ曲線、/ で区切って2つ書くと3次ベジェ曲線になります。

続く smooth は、制御点を自分で書かなくても、前の曲線となめらかにつながる曲線を引きます。CSS Shapes の仕様には、前の曲線の制御点を、今いる点を中心に反対側へ折り返した点を使うと書かれています。この例では、75% 100% を下の辺の真ん中で折り返した 25% calc(100% - 60px) です。そのため、右半分の谷と同じ大きさの山が、左半分にできます。

水色の帯の下の辺の波に、点と破線を重ねた図。右端と真ん中と左端に黒い丸があり、真ん中の丸の下に「1つ目の終点」。右側の谷の下に、黒い四角の「curve の制御点(75% 100%)」。左側の山の上に、ピンクの四角の「smooth が自動で使う点」。それぞれの四角から両側の黒い丸へ破線が引かれている
幅 368px・高さ 200px の要素に波の形を付け、曲線を決める点を重ねたもの。点と破線は画像用に描き足した

検証環境で、600×200px の要素に付けて下の辺の位置を調べました。両端と真ん中は上から 169px、右側の谷は 184px、左側の山は 154px でした。smooth の代わりに curve to 0 calc(100% - 30px) with 25% calc(100% - 60px) と書いた形とも比べると、9か所で調べた下の辺の位置がすべて同じでした。

高さを 120px に変えても、波の深さは変わりません。両端は 89px、谷は 104px、山は 74px で、上下に 15px ずつの波のままでした。calc(100% - 30px) で、下の辺から 30px の帯の中に波を描いているためです。

arc で、上の辺をアーチにする

円の一部を描くときは arc を使います。of のあとに半径を書きます。

css
.arch {
  width: 200px;
  height: 300px;
  clip-path: shape(
    from 0 100%,
    vline to 100px,
    arc to 100% 100px of 100px cw,
    vline to 100%,
    close
  );
}

左下から、左の辺に沿って上から 100px の位置まで線を引き、右の辺の同じ位置まで、半径 100px の円弧を引きます。検証環境では、上の辺の位置は、真ん中の列で 0px、左右の端から 15px の列で 47px でした。上が丸いアーチになります。

円弧の向きは cw(時計回り)か ccw(反時計回り)で指定します。省略したときは ccw です。上の例で cw を ccw に変えたり省いたりすると、円弧が下にふくらみ、真ん中の列は上から 200px までが空白になりました。

点線で囲んだ2つの縦長の枠。左の cw は、上の辺が半円にふくらんだアーチ。右の ccw(省略時)は、上から半円の形にくり抜かれ、下だけが水色で残っている
幅 160px・高さ 200px の要素に、cw と ccw の円弧を付けたもの。点線は要素の範囲

半径が小さすぎて2つの点を結べないときは、結べる大きさまで半径が広がります。MDN の shape() のページにも、この動きが書かれています。400×100px の要素で、横に 200px 離れた2点を半径 60px の円弧で結ぶと、円弧の深さは 99px でした。半径 100px の半円と同じ深さです。

半径を % 1つで書くときの注意

of 50% のように半径を % で1つだけ書くと、要素の幅に対する割合にはなりません。CSS Shapes の仕様では、circle() と同じく、対角線の長さを √2 で割った大きさ(sqrt(幅² + 高さ²) / sqrt(2))に対する割合になります。

400×100px の要素では、この大きさは約 291.55px で、その 50% は約 145.77px です。この半径で、横に 200px 離れた2点を結ぶと、円弧の深さは計算上 39.7px になります。検証環境で測ると 39px でした。半径が幅の 50%(200px)なら、深さは計算上 26.8px で、測った値とは合いません。

横と縦の半径を of 50% 50% のように2つ書くと、横は幅の 50%、縦は高さの 50% になります。同じ2点を結んだ円弧の深さは 6px と浅くなりました。

path() との違いは、大きさに合わせて伸び縮みすること

曲線を含む形は、これまでも path() で書けました。path() は SVG の d 属性と同じ文字列を書く関数で、座標は px です。

css
/* 幅 300px・高さ 150px の要素に合わせて書いた波 */
.wave-path {
  clip-path: path("M 0 0 H 300 V 120 Q 225 150 150 120 T 0 120 Z");
}

検証環境で、この path() と、上の節の shape() の波を、同じ大きさの要素に付けて比べました。幅 300px・高さ 150px の要素では、5か所で調べた下の辺の位置がすべて同じでした。

幅を 500px にすると、違いが出ました。shape() の波は幅いっぱいに広がりましたが、path() の形は幅 300px 用の大きさのままで、左から 375px と 499px の列には何も表示されませんでした。

上下に並べた2つの帯。上の「幅 240px 用に書いた path()」は、左から全体の3分の2ほどのところで帯が切れ、右側は白い。下の「% で書いた shape()」は、幅いっぱいに波の帯が広がっている
幅 368px の要素に、幅 240px 用に書いた path() と、% で書いた shape() を付けたもの

MDN では、shape() の利点として、ふだんの CSS の書き方で書けることを挙げています。%、rem、em などの単位や、calc()、max()、abs() などの数学関数も使えます。path() は1つの文字列で、単位は px に限られます。幅の決まっていない要素を曲線で切り抜くなら、shape() が向いています。

形をアニメーションさせる条件

shape() の形は、transition で少しずつ変えられます。次の例では、.on が付くと、まっすぐだった下の辺が下にふくらみます。

css
.box {
  transition: clip-path 1s linear;
  clip-path: shape(from 0 0, hline to 100%, vline to 70%,
    curve to 0 70% with 50% 70%, close);
}

.box.on {
  clip-path: shape(from 0 0, hline to 100%, vline to 70%,
    curve to 0 70% with 50% 100%, close);
}

検証環境で、.on を付けたあと、アニメーションを 500ms の位置で止めて getComputedStyle() で読むと、制御点は 50% 85% でした。70% と 100% のちょうど真ん中です。

途中の形が計算されるには、2つの形の命令の数と種類、並びがそろっている必要があります。CSS Shapes の仕様の補間の節では、to と by の違いや、curve と smooth の制御点の数もそろえることになっています。検証環境では、三角形(line が2つ)と四角形(line が3つ)のあいだや、line to と line by が入れ替わった形のあいだでは、途中の値が作られず、50% の時点ですでに終わりの形になっていました。

path() と shape() のあいだでも、命令がそろっていれば途中の形が作られます。検証環境では、途中の値は shape() の形で返り、px と % を合わせた calc(50% + 100px) のような座標になりました。

対応しているブラウザー

互換性データでは、shape() は次のバージョンから対応しています。

ブラウザー 対応
Chrome・Edge 135 から
Chrome(Android) 135 から
Firefox 148 から
Safari 18.4 から
Safari(iOS) 18.4 から

対応していないブラウザー向けに別の形を用意するときは、@supports で分けます。

css
.hero {
  clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); /* 直線だけの形 */
}

@supports (clip-path: shape(from 0 0, line to 100% 0)) {
  .hero {
    clip-path: shape(
      from 0 0,
      hline to 100%,
      vline to calc(100% - 30px),
      curve to 50% calc(100% - 30px) with 75% 100%,
      smooth to 0 calc(100% - 30px),
      close
    );
  }
}

検証環境の Chromium 153 では、@supports の中の shape() の指定が使われました。

​Posted by 86