Chrome 147で追加されたCSSの新機能を整理してみた(border-shape / contrast-color() / 要素スコープView Transitions)
どうも!オペ部の西村祐二です!
最近あらためてCSSを勉強しなおしており、新しい仕様のキャッチアップをしています。その流れで Chrome 147 stable に新しい CSS 機能がいくつか追加されていたので触ってみました。いずれも現場で使えそうな機能だったので、公式情報を読みつつ整理してみました。
今回対象にするのは次の3つです。
border-shapeプロパティcontrast-color()CSS 関数- 要素スコープの View Transitions
Chrome 147 とは
Chrome 147 は 2026年4月7日に「新機能紹介」記事が公開された stable リリースです。CSS まわりでは前述の3機能に加え、いくつかの Web Platform 機能がまとめてリリースされています。
新機能
border-shape プロパティ
矩形でないボーダーを作るための新しい CSS プロパティです。これまでは clip-path や疑似要素で似たような表現をしていましたが、border-shape ではボーダー自体を任意の形状として定義できるようになりました。
clip-path は要素を切り抜くだけなので、border / box-shadow / outline / border-image も一緒に切れてしまい、影や罫線が途中で途切れる問題がありました。border-shape は「ボックスの形状そのもの」を再定義するプロパティなので、背景・ボーダー・影・フォーカスアウトラインが新しい形状に追従します。仕様策定者(Una Kravets / Temani Afif)も、吹き出し・チケット・スカロップカードなど「装飾を残したまま非矩形にしたい UI」を主要ユースケースとして挙げています。
clip-path と同じ形状シンタックスが使えます。1つの形状を指定すれば外形、2つ指定すれば外形と内形(ボーダー内側)を独立に定義できます。
構文:
/* 1つ目: 外形 / 2つ目(省略可): 内形 */
border-shape: <basic-shape> [ <basic-shape> ]?
<basic-shape>:circle()/ellipse()/polygon()/inset()/path()/shape()など、clip-pathと同じ形状関数- 1つ目: ボーダーの外形(
border-boxに相当) - 2つ目(省略可): ボーダーの内形(
padding-boxに相当)。省略すると外形にborder-width分内側にオフセットした形状が自動的に使われる - 初期値は
none(通常の矩形ボーダー) border-shapeがnone以外のとき、border-radius(およびcorner-shape)は無視される(0として扱われる)
1. 円形ボタン
従来は border-radius: 50% で角を丸める書き方でした。
.orb-btn {
border: 8px solid #8b5cf6;
border-radius: 50%; /* 角を半径50%で丸めることで真円に見せる */
}
border-shape では形状そのものを指定します。
.orb-btn {
border: 8px solid #8b5cf6;
/* circle(<半径> at <中心x> <中心y>): 中心(50%,50%)・半径50%の真円 */
border-shape: circle(50% at 50% 50%);
}

2. ダイヤ型バッジ
従来は要素を rotate(45deg) で回して、内側テキストを逆回転させて戻していました。
.diamond {
transform: rotate(45deg); /* 要素を45度回してダイヤに見せる */
border: 14px solid #0ea5e9;
border-radius: 12px; /* 頂点を少し丸める */
}
.diamond > span {
transform: rotate(-45deg); /* 中のテキストは逆回転で元に戻す */
}
border-shape なら外形と内形を shape() で直接指定でき、回転トリックが不要になります。
.diamond {
border: 14px solid #0ea5e9;
border-shape:
/* 外形: 4頂点を8%の弧で丸めたダイヤ */
shape(
from 45% 5%,
arc to 55% 5% of 8% cw, /* 上の頂点を丸める */
line to 95% 45%,
arc to 95% 55% of 8% cw, /* 右の頂点を丸める */
line to 55% 95%,
arc to 45% 95% of 8% cw, /* 下の頂点を丸める */
line to 5% 55%,
arc to 5% 45% of 8% cw, /* 左の頂点を丸める */
close
)
/* 内形: 一回り小さい丸みダイヤ */
shape(
from 47% 18%,
arc to 53% 18% of 6% cw,
line to 82% 47%,
arc to 82% 53% of 6% cw,
line to 53% 82%,
arc to 47% 82% of 6% cw,
line to 18% 53%,
arc to 18% 47% of 6% cw,
close
);
}
shape() 関数は clip-path でも使える新しい形状記述で、line(直線)/arc(円弧)/curve(ベジェ曲線)などのコマンドを組み合わせて任意形状を記述できます。arc to X Y of R cw は「現在地から (X, Y) まで半径 R の円弧を時計回りに描く」意味です。

3. 吹き出しカード
従来は疑似要素で三角の「しっぽ」を付け足していました。
.bubble {
position: relative;
border: 8px solid #f97316;
border-radius: 12px;
}
/* 疑似要素で三角を作り、本体の下にくっつけて「しっぽ」に見せる */
.bubble::after {
content: "";
position: absolute;
bottom: -22px; left: 24px;
border: 12px solid transparent;
border-top-color: #f97316;
}
border-shape ならしっぽまで含めた形状を1つのボーダーとして定義できます。
.bubble {
border: 8px solid #f97316;
border-shape:
/* 外形: 本体の4隅を角丸(8%) + 直線のしっぽ */
shape(
from 8% 0%,
line to 92% 0%,
arc to 100% 10% of 8% cw, /* 右上角 */
line to 100% 75%,
arc to 92% 85% of 8% cw, /* 右下角 */
line to 22% 85%, /* しっぽ付け根(右) */
line to 12% 100%, /* しっぽ先端 */
line to 16% 85%, /* しっぽ付け根(左) */
line to 8% 85%,
arc to 0% 75% of 8% cw, /* 左下角 */
line to 0% 10%,
arc to 8% 0% of 8% cw, /* 左上角 */
close
)
/* 内形: しっぽなしの角丸矩形 */
shape(
from 8% 4%,
line to 92% 4%,
arc to 96% 10% of 6% cw,
line to 96% 75%,
arc to 92% 81% of 6% cw,
line to 8% 81%,
arc to 4% 75% of 6% cw,
line to 4% 10%,
arc to 8% 4% of 6% cw,
close
);
}
座標は x% y% で、左上が 0% 0%、右下が 100% 100% です。shape() では from で開始点を指定し、以降 line / arc / curve で現在地から次の点へ描いていきます。
polygon() や shape() の座標計算は慣れるまで直感的に書きづらく、頂点の数が増えるほど手書きはしんどいですが、AI に「こういう形状にしたい」と伝えればそれなりの叩き台を出してくれるので、実装のハードルはだいぶ下がった印象です。実際この記事のサンプルも、形のイメージを伝えて AI に生成させたあと、表示を見ながら座標を微調整する進め方で書きました。

他ブラウザの対応状況は進行中と考えられるため、実プロダクトで使う際は @supports で分岐させる使い方が無難そうです。
適したユースケース:
- クーポン・チケット・会員バッジなど、装飾的な UI パーツ
- プロモーションバナーの矢羽根タグ、セール訴求用のリボン
- ゲーミフィケーション(実績バッジ・ランクアイコン)
- これまで画像書き出しや SVG で対応していたキービジュアルの一部
矩形では表現しにくい「楽しさ」「おまけ感」を伴う装飾に向いていそうです。一方で、角丸+シンプルな罫線で十分な UI には過剰なので、使いどころを絞るのが良さそうです。
contrast-color() CSS 関数
アクセシビリティ向けの新しい色関数です。引数で指定した色に対して、コントラストが高い側の白または黒を返してくれます。
従来は JS で背景色の輝度を計算して文字色を切り替えたり、テーマごとに CSS 変数を手動で用意したりしていました。
// 背景色の輝度からテキスト色を決める
function pickTextColor(bg) {
const [r, g, b] = bg;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6 ? "#000" : "#fff";
}
element.style.color = pickTextColor([100, 150, 200]);
contrast-color() を使うと CSS 単体で同じことが書けます。
.button {
background: rgb(100, 150, 200);
/* 背景色に対してコントラストが高い側(白 or 黒)が自動で入る */
color: contrast-color(rgb(100, 150, 200));
}
ポイントは 文字色を個別に指定しなくても、ブラウザが背景色の明度を見て白か黒を自動で選んでくれる ことです。たとえば背景色を CSS 変数で受け取る汎用コンポーネントの場合、従来は色ごとに .tag-yellow { color: #000 } .tag-indigo { color: #fff } のように書き分ける必要がありましたが、color: contrast-color(var(--c)) の一行で済みます。
実際に5色のスウォッチで試したところ、以下のように自動で文字色が切り替わりました。
| 背景色 | 文字色(自動) |
|---|---|
Indigo #6366f1 |
黒 |
Yellow #eab308 |
黒 |
Pink #ec4899 |
黒 |
Slate 100 #f1f5f9 |
黒 |
Slate 800 #1e293b |
白 |

基本的には「明るい色には黒、暗い色には白」が入ります。ただし中間明度の色(Indigo や Pink のような白・黒どちらもコントラスト比が近い色)ではブラウザの判定が直感とズレることもあります。厳密なコントラストが必要な場面では手動指定で調整するのが無難です。
とはいえ、ダークモード対応やカスタムテーマで、背景色に応じてテキスト色を切り替えるロジックを JS や preprocessor 側に持っていたケースを、CSS 単体で完結できる可能性があります。
どのあたりで白・黒が切り替わるか気になったので、カラーピッカーで選んだ色を背景にしたときに contrast-color() が白と黒のどちらを返すかを可視化する確認用サイトを作って挙動をチェックしてみました。色をスライドさせていくと、どの明度で切り替わるかが一目で分かります。

適したユースケース:
- ユーザー定義色のタグ/ラベル
- グラフ・チャートの凡例や値ラベル(系列色が動的に決まる BI ダッシュボード)
- SaaS のマルチテナントで、テナントごとにブランドカラーを受け入れる UI
- カラーピッカーで選んだ色をそのまま背景に使うケース
要素スコープの View Transitions
これまでの View Transitions API はドキュメント全体に対してトランジションをかける仕様でしたが、Chrome 147 では任意の要素に限定してトランジションを適用できるようになりました。
構文:
// コールバックを直接渡す形
const transition = element.startViewTransition(updateCallback);
// options オブジェクトで渡す形
const transition = element.startViewTransition({
update: updateCallback, // DOMを変更する関数(必須)
types: ['reorder'] // transition の種別(任意)
});
updateCallback: DOMを書き換える関数。Promiseを返すことも可能で、完了を待ってから新しい状態を描画types::active-view-transition-type()セレクタで種別ごとに異なるアニメを当てたい場合に指定- 戻り値は
ViewTransitionオブジェクト。.ready/.finished/.skipTransition()などで遷移を制御できる document.startViewTransitionと違い、element(任意のDOM要素)に対して呼ぶことでページ全体をロックせずにその要素のサブツリーだけをアニメーション対象にできる
element.startViewTransition(() => {
// この要素配下のDOM更新のみをトランジション対象にする
});
ページ全体をロックせずに部分更新だけをアニメーションさせられる点がポイントです。ネストしたトランジションや並行トランジションも実行できるため、SPA 内の複雑な遷移に使いやすそうです。
実装例: タスクリストの並び替え
タスクカードを並び替えた際、各カードに view-transition-name を付けておくとブラウザが「どのカードがどこに移動したか」を自動補間してくれます。
<div id="list">
<div class="item" style="view-transition-name: vt-item-1">タスクA</div>
<div class="item" style="view-transition-name: vt-item-2">タスクB</div>
<div class="item" style="view-transition-name: vt-item-3">タスクC</div>
</div>
<button id="shuffle">並び替え</button>
const listEl = document.getElementById('list');
document.getElementById('shuffle').addEventListener('click', () => {
// list 要素に対してのみ transition を発動
listEl.startViewTransition(() => {
// DOMの並び替えを通常どおり実行するだけでよい
const items = [...listEl.children].sort(() => Math.random() - 0.5);
items.forEach(el => listEl.appendChild(el));
});
});
/* transition の時間はこれで調整 */
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.4s;
}
ポイントは document.startViewTransition ではなく listEl.startViewTransition と要素に対して呼び出すこと。この間ページの他の部分はロックされず、スクロールやフォーム入力をそのまま継続できます。

適したユースケース:
- カンバン/タスクリストの並び替えやドラッグ後の順序変更
- 通知センター、コメントリストなどの差し込みアニメーション
- カード詳細の展開・折りたたみ、モーダルの開閉
- SPA 内のタブ切替・ページネーション
実装自体は startViewTransition() で DOM 更新を包むだけで動くので、コストの割に画面遷移の滑らかさが目に見えて上がります。タスクリストのデモを触っていても、並び替えや追加時の動きがスーッと繋がるだけで「触っていて気持ちいい」と感じられる画面になりました。
まとめ
Chrome 147 の CSS 新機能3つは、いずれも JS や画像、疑似要素で手当てしていた表現を CSS に寄せる方向の追加でした。
border-shape: 装飾的な非矩形 UI をボーダーや影を保ったまま表現できるcontrast-color(): 動的な背景色に対する文字色の出し分けを CSS 単体で書ける- 要素スコープ View Transitions: ページ全体をロックせず部分的にアニメーションできる
他ブラウザの対応はこれからなので @supports 前提での段階導入になりますが、いずれもコードが減って見通しが良くなる追加なので、先行して検証しておく価値はありそうです。
誰かの参考になれば幸いです。
参考リンク:









