CSSのshape-outsideで、画像の形に文章を回り込ませる

​​CSS​CORS,float,Playwright

float で左に寄せた画像に shape-outside を付けると、文章が画像の四角い枠ではなく、円や透明な PNG の輪郭に沿って回り込みます。円なら shape-outside: circle(50%) を付け、見た目も border-radius: 50% で丸くします。画像の輪郭を使うときは、画像が CORS の条件を満たしていないと、回り込みが四角に戻ります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。フォントは Noto Sans CJK JP です。幅 360px、文字の大きさ 16px、行の高さ 28px の段落の左に、160px 四方の画像を float で置き、各行の1文字目の位置を測りました。ページはローカルの HTTP サーバーから開いています。Firefox と Safari での表示、縦書きは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

円に沿って回り込ませる

基本の形は、float した要素に shape-outside を足すだけです。

css
.photo {
  float: left;
  width: 160px;
  height: 160px;
  shape-outside: circle(50%);
  border-radius: 50%;
}

検証環境で、指定なしと circle(50%) を比べました。数字は、行の1文字目が段落の左端から何 px の位置にあるかです。

行 指定なし circle(50%)
1行目 160px 140.8px
2行目 160px 156.3px
3行目 160px 160px
4行目 160px 159.9px
5行目 160px 153.3px
6行目 160px 132.9px

指定なしでは、画像の横の6行がどれも 160px から始まりました。circle(50%) では、円の上下にあたる1行目と6行目が左へ寄り、文章が円の輪郭をなぞる形になりました。1行の高さ(28px)の中で、円がいちばん右に張り出す位置に合わせて、書き出しが決まっています。たとえば1行目の下端は円の中心から 52px 上で、そこでの円の右端がちょうど 140.8px です。

shape-outside が変えるのは、文章が回り込む線だけです。画像の見た目は変わりません。下の画像の真ん中のように、circle(50%) だけでは四角い画像の角に文字が重なります。見た目も丸くするには、border-radius: 50% をいっしょに付けます。CSS Shapes の仕様でも、形を指定してもボックスモデルそのものは変わらないと書かれています。

3つの段落を縦に並べた画面。上の「指定なし」は、水色の四角の右に、文章が同じ位置から始まる6行で並ぶ。中の「shape-outside: circle(50%) だけ」は、文章が丸く回り込んでいるが、四角の右上と右下の角に「猫」「借」などの文字が重なっている。下の「border-radius: 50% と余白(12px)を追加」は、水色の円の右に、円との間に少し空きを残して文章が回り込んでいる
上から、指定なし、shape-outside: circle(50%) だけ、border-radius と余白を足したもの。水色の部分が float した要素

float を付けないと、shape-outside は何もしません。CSS Shapes の仕様の shape-outside の項で、このプロパティが効くのは float した要素(と、initial-letter で作るボックス)とされています。検証環境で float: none にすると、計算値は circle(50%) のままでしたが、段落は画像の下(上から 162px の位置)から始まり、回り込みませんでした。

円と文章の間に余白を空ける

文字が円にくっついて見えるときは、shape-margin で形の外側に余白を足します。ただし、shape-margin だけを足しても、思ったほど離れません。

検証環境で circle(50%) に shape-margin: 12px を足すと、1行目は 155.9px、6行目は 149.7px と右へずれました。ところが2〜5行目は 160px のままで、円の右端から 12px 離れた 172px にはなりませんでした。CSS Shapes の仕様の shape-margin の項では、形は float した要素のマージンボックス(margin を含めた外枠)で切り取られ、shape-margin を足してもその外には広がらないと定められています。160px 四方の要素では、160px より右に余白を作れません。同じ項の注記にも、切り取られないように、shape-margin といっしょに margin を足す必要がある場合がある、と書かれています。

余白をきちんと空けるには、要素に margin-right を付けて枠を広げ、円の大きさは content-box で画像の範囲に固定します。

css
.photo {
  float: left;
  width: 160px;
  height: 160px;
  margin-right: 12px;
  shape-outside: circle(50%) content-box;
  shape-margin: 12px;
  border-radius: 50%;
}

この指定では、3行目が 172px、4行目が 171.9px から始まり、円の右端から 12px 離れました。上の画像のいちばん下が、この指定の表示です。

content-box を付けたのは、circle(50%) の 50% が何に対する割合かを決めるためです。MDN の shape-outside のページにあるとおり、何も書かないと、基準になる枠は margin を含めた margin-box になります。検証環境で、content-box を付けずに margin-right: 20px を足すと、円が右へずれて大きくなり、3行目の書き出しが 175.1px になりました。画像の右端(160px)を越えて、見た目の円とは合わない位置です。circle(50%) content-box に変えると、margin がないときと同じ 140.8px、156.3px、160px…の並びに戻りました。

border-radius: 50% を付けた要素なら、shape-outside: margin-box のように枠の名前だけを書いても、角の丸みに沿って回り込みます。検証環境では、circle(50%) と同じ書き出しの位置になりました。

三角形などの形は polygon() で作る

円でない形は、polygon() に頂点の座標を並べて作ります。左上が直角の三角形なら、次のように書きます。見た目も同じ形にするため、clip-path に同じ座標を渡しています。

css
.photo {
  float: left;
  width: 160px;
  height: 160px;
  shape-outside: polygon(0 0, 100% 0, 0 100%);
  clip-path: polygon(0 0, 100% 0, 0 100%);
}

検証環境では、1行目から6行目の書き出しが 160px、132px、104px、76px、48px、20px と、1行ごとに 28px ずつ左へ寄りました。斜めの辺に沿って、文章が階段状に回り込みます。楕円の ellipse() や、四角の内側を指定する inset() も、MDN の shape-outside のページに使える関数として載っています。

透明な PNG の輪郭に沿わせる

shape-outside に画像の URL を渡すと、その画像の透明でない部分の輪郭に沿って回り込みます。背景を透明にした PNG なら、描かれた形をそのまま使えます。

css
.photo {
  float: left;
  width: 160px;
  height: 160px;
  shape-outside: url("cat.png");
}

検証では、透明な背景に座った猫の形を描いた 200px 四方の PNG を作り、160px に縮めて表示しました。小さな頭の横にあたる1〜2行目は 87px と 88px から、大きな体の横にあたる4〜5行目は 153px と 154px から始まり、形に沿って書き出しが変わりました。shape-margin: 8px を足すと、1行目は 95px に右へずれました。

2つの段落を縦に並べた画面。点線の枠は160px四方の画像の範囲。上の「shape-outside: url(cat.png)」では、水色の猫の形の小さな頭の横から文章が始まり、大きな体の横では右へ下がって、形に沿って回り込んでいる。下の「別オリジンの画像を CORS ヘッダーなしで指定」では、同じ猫の画像なのに、文章は点線の枠の右端にそろって四角く回り込んでいる
上は同じオリジンの PNG を shape-outside に指定したもの。下は別オリジンの PNG を CORS ヘッダーなしで指定し、回り込みが四角に戻ったもの

形の境目は、各ピクセルの不透明度で決まります。初期値では、完全に透明でないピクセルはすべて形に入ります。半透明の部分を含む画像では、shape-image-threshold で、どの不透明度から形に含めるかを 0〜1 の数で決めます。検証環境で、左が不透明で右に向かって透明になるグラデーションを shape-outside に渡すと、初期値では6行とも 160px から始まり、四角い枠と同じでした。shape-image-threshold: 0.5 にすると、不透明度が半分を超える左半分だけが形になり、書き出しが 80px にそろいました。

画像の輪郭が使われず、四角に戻るとき

画像を使う回り込みは、条件を満たさないと、画面上は何の知らせもなく四角に戻ります。CSS のエラーにはならず、計算値も url(...) のままです。

CSS Shapes の仕様では、shape-outside の画像は CORS の手順で取得し、取得に失敗したときは none と同じ扱いにすると定められています。MDN の shape-outside のページにも、画像は利用を許可する CORS ヘッダーを付けて配信する必要があると書かれています。

検証環境で、ページとは別のポート(別オリジン)から同じ猫の画像を読み込むと、Access-Control-Allow-Origin ヘッダーがないときは、6行とも 160px から始まりました。ブラウザーのコンソールには、次のエラーが出ていました。

text
Access to image at 'http://localhost:8081/cat.png' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

同じ画像を Access-Control-Allow-Origin: * を付けて配信すると、同じオリジンのときと同じ 87px、88px、120px…の回り込みになりました。画像を CDN などの別ドメインに置くときは、CORS ヘッダーが付いているかを確かめます。

HTML ファイルをブラウザーで直接開いた場合(file:// のページ)も、同じフォルダーの画像なのに四角に戻りました。コンソールには「Cross origin requests are only supported for protocol schemes: chrome, chrome-untrusted, data, http, https.」というエラーが出ました。手元で試すときも、ローカルの HTTP サーバーから開く必要があります。画像の輪郭で回り込まないときは、まずブラウザーのコンソールに CORS のエラーが出ていないかを見ます。

対応しているブラウザー

互換性データでは、shape-outside、shape-margin、shape-image-threshold のどれも、次のバージョンから対応しています。circle()、polygon()、画像、グラデーションの値も同じバージョンです。

ブラウザー 対応バージョン
Chrome 37
Edge 79
Firefox 62
Safari 10.1
Safari(iOS) 10.3

​Posted by 86