
はじめに
前回の Part1 では、コンテナクエリ、スクロールドリブンアニメーション、Subgrid、text-wrap を紹介しました。Part2 では引き続き、2025年の開発現場で役立つ CSS の新機能を取り上げます。今回紹介するのは以下の4つです。
- :has() セレクター
- @starting-style と transition-behavior
- light-dark() と相対カラー構文
- interpolate-size(height: auto へのアニメーション)
CSS の紹介
:has() セレクター
:has() は子要素や後続要素の状態に応じて親要素をスタイリングできる機能です。従来の CSS では親から子への一方向のみでしたが、:has() により子の状態を条件に親を選択できるようになりました。
実装手順
1. 基本構文を理解する
:has() は引数に渡したセレクターにマッチする要素を持つかどうかを判定します。セレクターを引数として受け取り、その条件を満たす要素の有無でスタイルを適用します。
/* img を含む .card を選択 */ .card:has(img) { /* スタイル */ } /* :checked 状態の input を含む label を選択 */ label:has(input:checked) { /* スタイル */ }
2. 結合子を活用する
:has() 内で結合子を使うことで、より細かい条件を指定できます。
| 書き方 | 説明 |
|---|---|
:has(> img) |
直接の子要素に img を持つ場合 |
:has(+ p) |
直後の兄弟要素が p の場合 |
:has(~ .error) |
後続の兄弟要素に .error がある場合 |
:has(input:invalid) |
子孫に無効な input を持つ場合 |
/* 直接の子要素に img を持つ場合のみ */ .card:has(> img) { grid-template-columns: 200px 1fr; } /* 直後に p が続く h2 */ h2:has(+ p) { margin-bottom: 0.5rem; }
3. 否定と組み合わせる
:not() と組み合わせることで「〜を含まない」という条件も表現できます。
/* img を含まない .card */ .card:not(:has(img)) { padding: 2rem; } /* 必須項目を含まないフォーム */ form:not(:has(:required)) { border: 1px solid #ccc; }
実用例
/* フォームバリデーションのためのスタイル */ .form-group { /* グループ内の要素の周囲にパディングを設定 */ padding: 1rem; /* バリデーション状態を示すための左ボーダーの初期設定(透明) */ border-left: 3px solid transparent; } /* 子孫要素に無効な状態(:invalid)のフォーム要素が含まれる場合のスタイル */ .form-group:has(:invalid) { /* バリデーションエラーを示すために左ボーダーを赤色に変更 */ border-left-color: #e53e3e; /* エラー状態を示すために背景色を薄い赤に変更 */ background-color: #fff5f5; } /* 子孫要素にフォーカスされている状態(:focus)のフォーム要素が含まれる場合のスタイル */ .form-group:has(:focus) { /* フォーカス状態を示すために左ボーダーを青色に変更 */ border-left-color: #3182ce; /* フォーカス状態を示すために背景色を薄い青に変更 */ background-color: #ebf8ff; } /* --- */ /* カードレイアウトの切り替えのためのスタイル */ .card { /* CSS Gridを使用して子要素を配置 */ display: grid; /* グリッドアイテム間の間隔を設定 */ gap: 1rem; /* カード全体のパディングを設定 */ padding: 1rem; /* カードの境界線を設定 */ border: 1px solid #ddd; } /* 直下の子要素に画像(> img)が含まれる場合のスタイル */ .card:has(> img) { /* 画像エリアとコンテンツエリアの2カラムレイアウト(例: 150pxの画像と残りの幅のコンテンツ) */ grid-template-columns: 150px 1fr; } /* 直下の子要素に画像(> img)が含まれない場合のスタイル */ .card:not(:has(> img)) { /* 画像がない場合はコンテンツのみの1カラムレイアウト */ grid-template-columns: 1fr; } /* --- */ /* チェックボックスの状態に基づいて兄弟要素の表示を制御するスタイル */ .option-group:has(input:checked) .option-details { /* チェックボックスがチェックされている場合、詳細コンテンツを表示 */ display: block; } .option-group:not(:has(input:checked)) .option-details { /* チェックボックスがチェックされていない場合、詳細コンテンツを非表示 */ display: none; }
フォームバリデーションの例では、入力欄が無効な状態(:invalid)のときに親要素である .form-group の見た目を変更しています。JavaScript を使わずに、入力状態に応じた視覚的フィードバックを実現できます。
@starting-style と transition-behavior
@starting-style と transition-behavior を使うと、display: none からのアニメーションを CSS だけで実現できます。従来は JavaScript が必須だったモーダルの表示アニメーションを、純粋な CSS で実装できるようになりました。
実装手順
1. @starting-style で初期状態を定義する
@starting-style ブロック内に、要素が display: none から表示された直後の初期スタイルを記述します。ブラウザはこの状態を「トランジションの始点」として認識します。
.modal { opacity: 1; /* 最終状態 */ } @starting-style { .modal { opacity: 0; /* ここからトランジションが始まる */ } }
表示されるとき、opacity: 0(透明)から opacity: 1(不透明)へとトランジションします。
2. transition-behavior で display をトランジション対象にする
通常、display プロパティはトランジションできません。display: none と display: block の間に中間値が存在しないためです。
transition-behavior プロパティを使うと、この制限を解除できます。
.modal { transition-property: opacity, display; transition-duration: 0.3s; transition-behavior: allow-discrete; /* display のトランジションを許可 */ }
| 値 | 説明 |
|---|---|
normal |
通常の動作。離散的なプロパティはトランジションしない(デフォルト) |
allow-discrete |
display などの離散的なプロパティもトランジション対象にする |
allow-discrete を指定すると、ブラウザは以下のように動作します。
| 状態変化 | display の切り替わるタイミング |
|---|---|
| 非表示 → 表示 | トランジション開始時(0%)に block になる |
| 表示 → 非表示 | トランジション終了時(100%)に none になる |
これにより、フェードアウトが完了してから display: none になるため、アニメーションが最後まで見えます。
transition ショートハンドを使う場合は、各プロパティの後に allow-discrete を記述します。
.modal { transition: opacity 0.3s, display 0.3s allow-discrete; }
すべてのプロパティをまとめて指定する場合は、all と transition-behavior を組み合わせます。
.modal { transition: all 0.3s; transition-behavior: allow-discrete; }
all はすべてのプロパティを対象にするキーワードです。この書き方はシンプルですが、意図しないプロパティもアニメーションする可能性があるため、対象を明確にしたい場合は個別指定を使います。
3. 非表示状態のスタイルを定義する
非表示時のスタイルを @starting-style と揃えることで、表示・非表示の両方向でスムーズなアニメーションになります。
.modal { display: block; opacity: 1; transition: opacity 0.3s, display 0.3s allow-discrete; } /* 非表示状態 */ .modal.hidden { display: none; opacity: 0; } /* 表示時の初期状態(@starting-style と .hidden を同じ値にする) */ @starting-style { .modal { opacity: 0; } }
実用例
/* モーダルダイアログ */ .modal-overlay { display: flex; align-items: center; justify-content: center; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); opacity: 1; transition: opacity 0.3s, display 0.3s allow-discrete; } .modal-overlay.hidden { display: none; opacity: 0; } @starting-style { .modal-overlay { opacity: 0; } } .modal-content { background: white; padding: 2rem; border-radius: 8px; }
<button onclick="document.getElementById('modal').classList.remove('hidden')"> 開く </button> <div class="modal-overlay hidden" id="modal"> <div class="modal-content"> <p>モーダルの内容</p> <button onclick="document.getElementById('modal').classList.add('hidden')"> 閉じる </button> </div> </div>
JavaScript 側では .hidden クラスの付け外しだけで済み、アニメーションロジックを CSS に集約できます。
以下のサンプルは「開く」ボタンをクリックするとモーダルがフェードインし、「閉じる」ボタンでフェードアウトします。 JavaScript はクラスの付け外しのみで、アニメーションはすべて CSS で制御しています。

light-dark() と相対カラー構文
light-dark() 関数と相対カラー構文を使うと、ダークモード対応や色の動的調整を CSS だけで完結できます。メディアクエリの重複記述が減り、カラーパレットの管理が効率化されます。
実装手順
1. color-scheme を設定する
light-dark() を使用するには、まず color-scheme プロパティでライトモードとダークモードの両方に対応することを宣言します。
:root { color-scheme: light dark; }
| 値 | 説明 |
|---|---|
light |
ライトモードのみ対応 |
dark |
ダークモードのみ対応 |
light dark |
両方に対応(システム設定に追従) |
2. light-dark() で明暗の色を一括指定する
light-dark() 関数は第1引数にライトモード時の色、第2引数にダークモード時の色を指定します。
:root { color-scheme: light dark; --bg-primary: light-dark(#ffffff, #1a1a1a); --bg-secondary: light-dark(#f5f5f5, #2d2d2d); --text-primary: light-dark(#333333, #e0e0e0); --text-secondary: light-dark(#666666, #a0a0a0); --border-color: light-dark(#dddddd, #404040); } body { background-color: var(--bg-primary); color: var(--text-primary); }
3. 相対カラー構文で色を動的に調整する
相対カラー構文を使うと、既存の色を基準に明度や彩度を調整した新しい色を生成できます。相対カラー構文は rgb、hsl など様々な色空間で使えますが、ここでは oklch を使います。
oklch は人間の知覚に基づいた色空間です。明度を変えても色味がずれにくい特徴があり、「ブランドカラーを少し明るく」といった調整に向いています。
| チャンネル | 意味 | 操作例 |
|---|---|---|
l |
明度(0〜1) | 増やすと明るく、減らすと暗く |
c |
彩度(0〜0.4程度) | 増やすと鮮やか、減らすとくすむ |
h |
色相(0〜360) | 色相環の角度 |
基本構文では from キーワードで基準色を指定します。
すると各チャンネルを取り出して自動計算されます。
:root { --brand-color: oklch(60% 0.15 250); /* 明度を上げる */ --brand-light: oklch(from var(--brand-color) calc(l + 0.2) c h); /* 明度を下げる */ --brand-dark: oklch(from var(--brand-color) calc(l - 0.2) c h); /* 彩度を下げる */ --brand-muted: oklch(from var(--brand-color) l calc(c - 0.05) h); /* 透明度を追加 */ --brand-transparent: oklch(from var(--brand-color) l c h / 0.5); }
余談ですが、Tailwind CSS はv3では対応していませんでしたが、v4で相対カラーに対応しました。
light-dark() により、メディアクエリを使わずにダークモード対応が完結します。相対カラー構文と組み合わせることで、ブランドカラーを1箇所で変更するだけで、ホバー状態やアクティブ状態の色も自動的に調整されます。
実用例
light-dark() でダークモード対応
:root { color-scheme: light dark; --surface-1: light-dark(#ffffff, #121212); --surface-2: light-dark(#f8f9fa, #1e1e1e); --text-1: light-dark(#212529, #f8f9fa); --text-2: light-dark(#495057, #adb5bd); --border: light-dark(#dee2e6, #373737); } .card { background: var(--surface-1); color: var(--text-1); border: 1px solid var(--border); padding: 1.5rem; border-radius: 8px; } .card-meta { color: var(--text-2); font-size: 0.875rem; }
メディアクエリなしで、システム設定に応じてライト/ダークが自動で切り替わります。
相対カラー構文でボタンのバリエーション生成
:root { --brand: oklch(55% 0.2 250); } .button { background: var(--brand); color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; transition: background 0.2s; } .button:hover { /* 明度を少し下げる */ background: oklch(from var(--brand) calc(l - 0.1) c h); } .button:active { /* さらに明度を下げる */ background: oklch(from var(--brand) calc(l - 0.15) c h); } .button.subtle { /* 彩度を下げて控えめに */ background: oklch(from var(--brand) 0.95 0.02 h); color: var(--brand); }
--brand を1箇所で変更するだけで、hover・active・subtle すべての色が自動的に調整されます。oklch なら明度を変えても青が紫になるような色ずれが起きません。

スライダーでベースカラーの色相(h)を変更すると、すべての派生色が連動して更新されます。 --brand を from で参照しているため、ベースを1箇所変えるだけで Light・Dark・Muted・ボタンのホバー色がすべて自動で調整されます。これが相対カラー構文の利点です。
interpolate-size(height: auto へのアニメーション)
interpolate-size は、長年の CSS の課題だった「height: auto へのトランジション」を解決する機能です。アコーディオンや「もっと見る」ボタンの展開アニメーションを、JavaScript による高さ計算なしで実装できます。
Note:
interpolate-sizeは Chromium 系ブラウザ(Chrome、Edge)でサポートされていますが、Safari・Firefox は未対応です(2025年12月現在)。ただし非対応ブラウザではアニメーションなしで即時切り替えとなるため、プログレッシブエンハンスメントとして導入できます。
実装手順
1. interpolate-size を設定する
interpolate-size: allow-keywords を指定すると、auto、min-content、max-content などの内在サイズ値へのトランジションが可能になります。
:root { interpolate-size: allow-keywords; }
| 値 | 説明 |
|---|---|
numeric-only |
既定値。内在サイズ値(auto など)は補間できない |
allow-keywords |
<length-percentage> と内在サイズ値の間の補間を許可 |
ページ全体に適用する場合は :root に、特定の要素のみに適用する場合は対象要素に指定します。
2. 通常のトランジションを適用する
interpolate-size: allow-keywords を設定した後は、通常どおり transition プロパティを使うだけです。
.expandable { height: 0; overflow: hidden; transition: height 0.3s ease; } .expandable.open { height: auto; }
3. calc-size() で計算を加える
calc-size() 関数を使うと、auto に対して計算を加えることができます。
.expandable.open { /* auto + 余白 */ height: calc-size(auto, size + 2rem); }
実用例
:root { interpolate-size: allow-keywords; } /* アコーディオン */ .accordion-item { border: 1px solid #ddd; border-radius: 4px; margin-bottom: 0.5rem; } .accordion-header { padding: 1rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; } .accordion-content { height: 0; overflow: hidden; transition: height 0.3s ease; } .accordion-item.open .accordion-content { height: auto; } /* 「もっと見る」展開 */ .text-preview { height: 4.5em; /* 3行分 */ overflow: hidden; transition: height 0.4s ease; } .text-preview.expanded { height: auto; } /* details/summary との組み合わせ */ details { border: 1px solid #ccc; border-radius: 4px; padding: 0.5rem 1rem; } details .content { height: 0; overflow: hidden; transition: height 0.3s ease, opacity 0.3s ease; opacity: 0; } details[open] .content { height: auto; opacity: 1; }
<!-- アコーディオン HTML --> <div class="accordion-item"> <div class="accordion-header"> セクション1 <span class="icon">▼</span> </div> <div class="accordion-content"> <div class="accordion-body"> コンテンツがここに入ります。 高さは内容に応じて自動的に決まります。 </div> </div> </div> <!-- details/summary HTML --> <details> <summary>詳細を見る</summary> <div class="content"> <p>展開されるコンテンツです。</p> </div> </details>
アコーディオンの例では、従来 JavaScript で scrollHeight を取得して高さを設定していた処理が不要になります。CSS だけで height: 0 から height: auto へのスムーズなトランジションが実現でき、コードの保守性が向上します。

クリックでアコーディオンが開閉します。height: 0 から height: auto へスムーズにトランジションする様子を確認できます。 従来は JavaScript で scrollHeight を取得して高さを計算する必要がありましたが、interpolate-size: allow-keywords を :root に設定するだけで、CSS のみで実現できます。JavaScript はクラスの付け外しだけです。 注意: 現時点では Chromium 系ブラウザ(Chrome、Edge)のみ対応しています。Safari・Firefox では即時切り替えとなりますが、開閉自体は問題なく動作します。
まとめ
Part2 では、2025年に実用レベルで使える CSS の新機能を紹介しました。
- :has() セレクター: 子要素の状態に応じて親をスタイリングでき、フォームバリデーションやレイアウトの条件分岐を CSS だけで実現できる
- @starting-style と transition-behavior:
display: noneからのアニメーションが可能になり、モーダルやトーストの実装が CSS で完結する - light-dark() と相対カラー構文: ダークモード対応を1行で記述でき、ブランドカラーから派生色を動的に生成できる
- interpolate-size: 長年の課題だった
height: autoへのトランジションが解決し、アコーディオンの実装がシンプルになる
:has()、@starting-style / transition-behavior、light-dark() / 相対カラー構文は主要ブラウザで広くサポートされています。interpolate-size は現時点では Chromium 系ブラウザ(Chrome、Edge)のみの対応ですが、プログレッシブエンハンスメントとして導入でき、非対応ブラウザでは即時切り替えにフォールバックします。まずは小さなコンポーネントから試してみて、JavaScript に頼っていた処理をどれだけ CSS に移行できるか確認してみてください。
次回の Part3 では、Anchor Positioning や View Transitions API など、少し先を見据えた機能を紹介します。お楽しみに!
暇があったらクライミングしているフロントエンドエンジニアです。