丸く切り抜いた画像の横に文章を流したのに、テキストの左端はまっすぐ揃ったまま——float による回り込みは要素のボックス(四角い領域)を基準にするため、見た目が円でも回り込みは四角いままです。これを解決するのが CSS の shape-outside で、回り込みの基準になる形そのものを円や多角形に変えられます。この記事では前提になる float との関係、指定できる値、shape-margin や url() を使った応用までを、動かせるデモ付きで解説します。
目次
shape-outside は float した要素にだけ効く
最初に押さえておきたいのは、shape-outside は float が指定された要素にしか効かないという点です。仕様でも適用対象は「浮動要素(floats)」と定められていて、それ以外の要素に書いても値は保持されるだけで何も起こりません。「書いたのに変わらない」という相談のほとんどは、これが原因です。
そもそも float は、指定した要素を左右に寄せて、後続のテキストをその横に流し込む仕組みです。このときテキストが避けるのは要素のマージンボックス、つまり余白まで含めた四角い領域になります。角丸や clip-path で見た目をどれだけ丸くしても、この四角い領域は変わりません。
shape-outside は、この「テキストが避ける領域」の形だけを差し替えるプロパティです。要素そのもののサイズや背景の形は一切変わらず、あくまで外側のコンテンツがどう流れるかだけを制御します。この役割分担を理解しておくと、後半で触れるつまずきどころもすんなり納得できるはずです。
基本の使い方|circle() で丸く回り込ませる
もっとも使う機会が多いのが、円形のサムネイルに沿ってテキストを流す形です。要素を float させ、見た目を border-radius: 50% で丸くしたうえで、shape-outside: circle(50%) を追加します。
.thumb {
/* 1. まず float させる(これが無いと shape-outside は効かない) */
float: left;
width: 160px;
height: 160px;
/* 2. 見た目を丸くする */
border-radius: 50%;
/* 3. 回り込みの形も丸くする */
shape-outside: circle(50%);
}
circle() の引数は半径です。パーセントで書いた場合は要素の幅と高さから求めた基準値に対する割合になり、正方形の要素なら 50% がちょうど内接する円になります。中心をずらしたいときは circle(50% at 30% 50%) のように at に続けて位置を書きます。
下のデモでボタンを押すと、shape-outside の有無を切り替えられます。無効にすると文章の左端が一直線に揃い、円の上下に無駄な余白ができることが確認できます。
<div class="demo">
<button type="button" id="toggle">shape-outside を無効にする</button>
<p class="demo__state">現在:<strong id="state">shape-outside: circle(50%)</strong></p>
<div class="article">
<div class="thumb" id="thumb"></div>
<p>丸い画像の横に文章を置くと、円の外側の四隅にどうしても隙間が空いてしまいます。これは float による回り込みが、要素の見た目ではなくボックス(四角い領域)を基準にしているためです。</p>
<p>shape-outside を指定すると、回り込みの基準になる形そのものを円や多角形に変えられます。ボタンを押して、文章の左端がどう変わるかを見比べてみてください。</p>
</div>
</div>
.demo {
font-family: sans-serif;
padding: 16px;
line-height: 1.9;
}
.thumb {
/* 1. shape-outside は float した要素にしか効かない */
float: left;
width: 160px;
height: 160px;
margin-right: 8px;
/* 2. 見た目を丸くする(背景の形) */
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #6ec1ff, #0059b3);
/* 3. 回り込みの形も丸くする(テキストの流れ) */
shape-outside: circle(50%);
}
/* ボタンで付け外しするクラス。初期値の none に戻す */
.thumb.is-off {
shape-outside: none;
}
#toggle {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
}
.demo__state {
font-size: 14px;
color: #555;
}
const thumb = document.getElementById('thumb');
const toggle = document.getElementById('toggle');
const state = document.getElementById('state');
toggle.addEventListener('click', () => {
const off = thumb.classList.toggle('is-off');
toggle.textContent = off ? 'shape-outside を有効にする' : 'shape-outside を無効にする';
state.textContent = off ? 'shape-outside: none(初期値)' : 'shape-outside: circle(50%)';
});
shape-outside に指定できる値
値は大きく分けて、形を関数で書く「基本図形」、基準にする四角い領域を選ぶ「参照ボックス」、そして画像から形を作る url() の 3 系統があります。
| 値 | 意味 |
|---|---|
none | 初期値。回り込みは要素のマージンボックス(四角)どおりになる |
circle() | 円。circle(半径 at 中心位置) の形で書く |
ellipse() | 楕円。ellipse(横半径 縦半径 at 中心位置) と、半径を 2 つ指定する |
inset() | 参照ボックスを内側にへこませた長方形。inset(20px 30px round 10px) のように round で角丸も付けられる |
polygon() | 多角形。polygon(0 0, 100% 50%, 0 100%) のように頂点の座標を 3 つ以上並べる |
url() | 画像を指定し、その透明な部分を除いた形に沿わせる。PNG など透過画像で使う |
margin-box / border-box / padding-box / content-box | 参照ボックス(<shape-box>)。形の基準にする領域を選ぶ。単独でも、基本図形と併記しても使える |
参照ボックスは少し分かりにくいので補足します。circle(50%) のようにパーセントで大きさを書くと、その基準になる四角い領域が必要になります。何も書かなければ margin-box(マージンまで含めた領域)が使われます。shape-outside: circle(50%) content-box; のように併記すれば、パディングやボーダーを除いた中身の領域を基準にできます。
参照ボックスだけを単独で書くこともできます。たとえば shape-outside: border-box; と指定すると、border-radius で丸めた角がそのまま回り込みの形として使われます。角丸のカードにテキストを寄り添わせたいときは、これだけで足りることもあります。
polygon() で斜めに回り込ませる
雑誌のような斜めのレイアウトを作りたいときは polygon() を使います。頂点を「左からの位置 上からの位置」の組で並べていく書き方で、0 0 が左上、100% 100% が右下です。
.slope {
float: right;
width: 180px;
height: 240px;
/* 見た目を三角形に切り抜く */
clip-path: polygon(100% 0, 100% 100%, 0 100%);
/* 回り込みの形にも同じ座標を指定する */
shape-outside: polygon(100% 0, 100% 100%, 0 100%);
}
ポイントは clip-path と 同じ座標を 2 回書いていることです。shape-outside は回り込みしか変えないため、これを省くと背景だけが四角く残ってしまいます。デモで実際の見え方を確認してみてください。
<div class="demo">
<div class="article">
<div class="slope"></div>
<p>polygon() は座標を「左からの位置 上からの位置」の組で並べて、自由な多角形を作れる関数です。時計回りでも反時計回りでも構いませんが、頂点は 3 つ以上必要です。</p>
<p>ここでは右上から左下へ向かう三角形を作り、その斜辺に沿って文章を回り込ませています。背景の形は clip-path で同じ座標を指定して切り抜いているので、見た目と回り込みの形がぴったり重なります。</p>
<p>shape-outside だけを指定すると背景は四角いままなので、こうしたデザインでは clip-path とセットで使うのが基本になります。</p>
</div>
</div>
.demo {
font-family: sans-serif;
padding: 16px;
line-height: 1.9;
}
.slope {
float: right;
width: 180px;
height: 240px;
background: linear-gradient(135deg, #ff9a5a, #e8452c);
/* 見た目を三角形に切り抜く */
clip-path: polygon(100% 0, 100% 100%, 0 100%);
/* 回り込みの形も同じ座標で指定する */
shape-outside: polygon(100% 0, 100% 100%, 0 100%);
/* 形と本文のあいだに余白を取る */
shape-margin: 12px;
}
座標は px や calc() でも書けます。頂点が 3 つ未満だと図形として成立せず値そのものが無効になるので、そこだけ注意してください。
shape-margin で形と本文のあいだに余白を取る
shape-outside だけを指定すると、文字が形にぴったり張り付いて窮屈に見えます。そこで使うのが shape-margin です。作った形の輪郭から指定した分だけ外側に広げた領域を、新しい回り込みの形として扱います。
.thumb {
float: left;
width: 150px;
height: 150px;
border-radius: 50%;
shape-outside: circle(50%);
/* 形の外側に 12px の余白を足す(初期値は 0) */
shape-margin: 12px;
}
名前は似ていますが margin とは別物です。margin は要素のボックスそのものを押し広げてレイアウトに影響しますが、shape-margin が太らせるのは回り込みの形だけで、要素の占める位置は変わりません。負の値は指定できず、パーセントで書いた場合は包含ブロックの幅が基準になります。
下のデモのスライダーで値を動かすと、円の全方向に均等に余白が広がっていきます。
<div class="demo">
<p class="demo__control">
<label for="range">shape-margin:<strong id="value">0px</strong></label><br>
<input type="range" id="range" min="0" max="40" value="0">
</p>
<div class="article">
<div class="ball" id="ball"></div>
<p>shape-margin は、shape-outside で作った形の外側に余白を足すプロパティです。margin とは別物で、あくまで「回り込みの形」だけを太らせます。</p>
<p>スライダーを動かすと、円と文章のあいだの隙間が全方向に均等に広がっていくのが分かります。0 のままだと文字が円に触れてしまい窮屈に見えるので、10px 前後を目安に入れておくと読みやすくなります。</p>
</div>
</div>
.demo {
font-family: sans-serif;
padding: 16px;
line-height: 1.9;
}
.ball {
float: left;
width: 150px;
height: 150px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #7de2a8, #128a54);
shape-outside: circle(50%);
/* 形の外側に余白を足す。初期値は 0 */
shape-margin: 0px;
}
.demo__control {
font-size: 14px;
}
#range {
width: 240px;
}
const ball = document.getElementById('ball');
const range = document.getElementById('range');
const value = document.getElementById('value');
range.addEventListener('input', () => {
ball.style.shapeMargin = range.value + 'px';
value.textContent = range.value + 'px';
});
url() で PNG の透過部分に沿わせる
切り抜き済みの商品写真やイラストのように、関数では表せない複雑な輪郭に沿わせたいときは url() で画像を指定します。ブラウザーは画像のアルファチャンネル(透明度)を読み取り、透明でない部分の形を回り込みの形として使います。
.photo {
float: left;
width: 240px;
/* 透過 PNG の不透明な部分に沿って回り込ませる */
shape-outside: url("/images/item.png");
/* どのくらい薄い部分まで形に含めるか(0〜1、初期値は 0) */
shape-image-threshold: 0.2;
shape-margin: 10px;
}
shape-image-threshold は、どの濃さから「形の内側」とみなすかのしきい値です。初期値の 0 では少しでも色が乗っているピクセルはすべて形に含まれます。影や輪郭のぼかしまで拾って形が大きくなりすぎるときは、0.2 や 0.5 のように上げていくと薄い部分が切り捨てられ、輪郭がはっきりします。逆に 1 にすると完全に不透明な部分しか残りません。
そして url() を使うときに最大の落とし穴になるのが CORS です。ブラウザーはピクセルを読み取る必要があるため、画像は同じオリジン(同じドメイン)にあるか、CORS で読み取りが許可されている必要があります。条件を満たさない画像を指定すると、その shape-outside は none として扱われ、回り込みは四角いままになります。派手なエラーも出ないので原因に気付きにくい部分です。
外部の CDN やストレージに置いた画像を使う場合は、配信側で Access-Control-Allow-Origin ヘッダーを返す設定が必要です。手っ取り早いのは、画像を自分のサイトと同じドメインに置いてしまうことです。なお、ローカルの file:// で開いた HTML も同一オリジンとみなされないことがあるため、確認はローカルサーバー経由で行ってください。
ちなみに url() の代わりにグラデーションを指定することもできます。shape-outside: linear-gradient(to bottom right, transparent 50%, black 50%); のように書くと、透明な側を避ける形で斜めに回り込みます。グラデーションは CSS が生成する画像なので、CORS の制限を受けません。
回り込みが変わらないときに見直すところ
shape-outside は「書いたのに何も起きない」が起こりやすいプロパティです。無効な値のときは静かに none として扱われるため、開発者ツールにも目立つ警告が出ません。よくある原因を順に確認していきましょう。
float が指定されていない
まず疑うべきはここです。position: absolute で配置した要素や、display: flex / display: grid のコンテナの子要素には効きません。フレックスアイテムやグリッドアイテムでは float の指定そのものが無視される決まりなので、親を flex にした瞬間に回り込みが消えた、というケースはよくあります。通常のブロックレイアウトの中で float: left または float: right を指定した要素であることを確認してください。
要素に幅と高さが無い
circle(50%) のようにパーセントで書いた形は、参照ボックスの大きさから計算されます。中身が空の <div> のように幅も高さも 0 の要素では、形も大きさ 0 になって何も避けられません。<img> を使う場合も、読み込みが終わるまでサイズが確定しないことがあります。まずは width と height を明示して、要素が確かに面積を持っているかを見てください。
背景が四角いまま残っている
「テキストは丸く回り込んでいるのに、画像や背景は四角い」という場合、それは不具合ではなく仕様どおりの動作です。繰り返しになりますが、shape-outside が変えるのは外側のテキストの流れだけで、要素自身の見た目には一切関与しません。見た目も合わせたいときは、border-radius や clip-path で要素側を切り抜き、shape-outside と同じ形を二重に指定する必要があります。
url() の画像が CORS を満たしていない
前の章で触れたとおり、別ドメインから読み込んだ画像は、CORS の許可が無いとピクセルを読み取れず none 扱いになります。画像そのものは問題なく表示されるので気付きにくいのですが、shape-outside だけが無効になります。画像を同じドメインに移すか、配信側のヘッダー設定を見直してください。
要素の中身は形の影響を受けない
shape-outside が効くのは、あくまで float した要素の外にあるコンテンツです。その要素の内側に入れたテキストは形に沿って流れず、これまでどおり四角い領域に配置されます。内側のテキストも形に沿わせたい場合は shape-inside という別のプロパティを使うことになりますが、こちらは対応しているブラウザーがまだ限られます。
まとめ
shape-outside は、float した要素の周りをテキストがどう流れるかを、円・楕円・多角形・画像の輪郭といった自由な形に変えられるプロパティです。円形サムネイルなら circle(50%)、斜めのレイアウトなら polygon()、複雑な切り抜き写真なら url() と使い分け、shape-margin で文字との距離を整えるのが基本の流れになります。
うまくいかないときは、float が効いているか・要素に大きさがあるか・見た目は clip-path などで別途切り抜いているかの 3 点を順に確認してください。このプロパティが担当しているのは回り込みの形だけ、と割り切って覚えておくと迷わずに使えます。