🎨

最近のCSS、全然追えてなかった。ここ1〜2年で使えるようになった機能10選

に公開

この記事では、2024〜2025年にかけて主要ブラウザで使えるようになったCSS(一部HTML)の機能を10個紹介します。「昔はセレクターとか頑張って追いかけてたけど、最近は全然追えてない」という方を対象としています。

それぞれの機能について、目的・できること・昔のやり方・注意点をまとめました。コードを見ながら「こんなの使えるようになったのか」と感じてもらえたら嬉しいです。

1. CSS Nesting(ネイティブCSSネスト)

目的

SassやLESSなどのプリプロセッサなしで、CSSにネスト(入れ子)構造を書けるようにすることです。

できること

セレクターを親子関係で入れ子にして記述できます。& を使って親セレクターを参照します。

.card {
  padding: 1rem;
  background: #fff;

  & .title {
    font-size: 1.25rem;
    font-weight: bold;
  }

  &:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }
}

昔のやり方

フラットにセレクターを並べるか、Sass等のプリプロセッサを使っていました。

.card {
  padding: 1rem;
  background: #fff;
}
.card .title {
  font-size: 1.25rem;
  font-weight: bold;
}
.card:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

注意点

  • ネストされたセレクターには & を付ける必要があります(Sassとは少し異なります)
  • Chrome 120以降では & なしでも要素セレクターをネストできますが、古いバージョンとの互換性を考慮すると & を付けておくのが安全です

2. :has() セレクター

目的

CSSだけで「子要素や後続要素の状態に応じて親要素をスタイリングする」ことを実現します。長年「親セレクター」と呼ばれて待望されていた機能です。

できること

ある要素が特定の子要素や隣接要素を持っているかどうかでスタイルを変えられます。

/* 画像を含むカードだけ横並びレイアウトにする */
.card:has(.card-image) {
  display: flex;
  gap: 1rem;
}

/* 入力欄が無効なとき、ラベルの色を変える */
label:has(+ input:disabled) {
  color: #999;
}

/* チェック済みのチェックボックスを含むリスト項目を装飾する */
li:has(input:checked) {
  background: #e8f5e9;
}

昔のやり方

JavaScriptで親要素を取得してクラスを付け替えていました。

const cards = document.querySelectorAll('.card');
cards.forEach(card => {
  if (card.querySelector('.card-image')) {
    card.classList.add('has-image');
  }
});

注意点

  • 複雑な :has() の組み合わせはパフォーマンスに影響する可能性があります
  • :has() の中に :has() をネストすることはできません

3. Container Queries(コンテナクエリ)

目的

メディアクエリがビューポート幅に基づくのに対し、コンテナクエリは親要素の幅に基づいてスタイルを切り替えます。コンポーネント単位のレスポンシブデザインが可能になります。

できること

/* 親要素をコンテナとして宣言する */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* コンテナの幅に応じてレイアウトを変える */
@container card (min-width: 400px) {
  .card {
    display: flex;
    gap: 1rem;
  }
}

@container card (min-width: 600px) {
  .card {
    grid-template-columns: 1fr 2fr;
  }
}

昔のやり方

メディアクエリでビューポート幅に応じたスタイルを書くか、ResizeObserverを使ってJavaScriptで制御していました。

/* ビューポート全体の幅で判定するしかなかった */
@media (min-width: 768px) {
  .card {
    display: flex;
  }
}

注意点

  • container-type: inline-size を設定した要素はコンテインメントが適用されるため、子要素の高さによって親の高さが決まらなくなる場合があります
  • コンテナクエリ単位(cqwcqh など)も一緒に使えます

4. Subgrid

目的

入れ子になったグリッドアイテムが、親グリッドのトラック(行や列)の定義を引き継げるようにします。カードリストのような場面で、各カード内の要素の高さを揃えたいときに便利です。

できること

.card-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* 各カードの内部構造に合わせた行定義 */
  grid-template-rows: auto 1fr auto;
  gap: 1.5rem;
}

.card {
  display: grid;
  /* 親のグリッド行定義を引き継ぐ */
  grid-template-rows: subgrid;
  grid-row: span 3;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

この例では、すべてのカードの見出し・本文・フッターの高さが自動的に揃います。

昔のやり方

入れ子のグリッドでは親のトラック定義を参照できなかったので、固定の高さを指定するか、JavaScriptで最大の高さを計算して揃えていました。

// 各行の最大高さを計算して揃える処理が必要だった
const titles = document.querySelectorAll('.card-title');
let maxHeight = 0;
titles.forEach(el => {
  maxHeight = Math.max(maxHeight, el.offsetHeight);
});
titles.forEach(el => {
  el.style.height = `${maxHeight}px`;
});

注意点

  • Subgridを使うには、子要素が grid-row: span Ngrid-column: span N で親のトラックに明示的にまたがっている必要があります

5. @layer(カスケードレイヤー)

目的

CSSの詳細度(specificity)の競合を根本的に解決するための仕組みです。レイヤーの宣言順序でスタイルの優先順位を制御できます。

できること

/* レイヤーの優先順位を宣言する(後に書いたものが高い) */
@layer reset, base, components, utilities;

@layer reset {
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}

@layer base {
  body {
    font-family: sans-serif;
    line-height: 1.6;
  }
}

@layer components {
  .btn {
    padding: 0.5rem 1rem;
    border-radius: 4px;
    background: #3b82f6;
    color: #fff;
  }
}

@layer utilities {
  .mt-4 {
    margin-top: 1rem;
  }
}

utilities レイヤーは最後に宣言されているため、components レイヤーよりも常に優先されます。詳細度の計算を気にする必要がなくなります。

昔のやり方

詳細度を上げるためにセレクターを長くしたり、最終手段として !important を使っていました。

/* 詳細度の戦い */
body main .content .card .btn {
  background: red;
}

/* 最終手段 */
.btn {
  background: red !important;
}

注意点

  • レイヤーに属さないスタイルは、すべてのレイヤーよりも優先されます
  • サードパーティのCSSライブラリを @layer でラップすると、自分のスタイルで簡単にオーバーライドできます

6. light-dark() 関数

目的

ライトモードとダークモードの色をひとつのプロパティでまとめて指定できる関数です。テーマ切り替えのためのCSSを大幅に簡潔にします。

できること

:root {
  color-scheme: light dark;
}

body {
  background-color: light-dark(#ffffff, #1a1a2e);
  color: light-dark(#333333, #e0e0e0);
}

.card {
  background: light-dark(#f5f5f5, #2a2a3e);
  border: 1px solid light-dark(#ddd, #444);
}

a {
  color: light-dark(#1a73e8, #8ab4f8);
}

ユーザーのOS設定に応じて、自動的にライト用・ダーク用の値が切り替わります。

昔のやり方

prefers-color-scheme メディアクエリで同じプロパティを二度書く必要がありました。

body {
  background-color: #ffffff;
  color: #333333;
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: #1a1a2e;
    color: #e0e0e0;
  }
}

注意点

  • color-scheme: light dark の宣言が必須です。これがないと light-dark() は機能しません
  • light-dark() が受け取れるのは <color> 値のみです。linear-gradient() などは直接渡せません

7. text-wrap: balance / pretty

目的

テキストの折り返しを美しく制御します。見出しの行バランスや、本文の孤立語(最終行に1単語だけ残る状態)を防ぎます。

できること

/* 見出しの行の長さを均等に揃える */
h1, h2, h3 {
  text-wrap: balance;
}

/* 本文の最終行に孤立語が残らないようにする */
p {
  text-wrap: pretty;
}

balance を適用した見出しのイメージです。

/* balance なし */
とても長い見出しのテキストがここに入り
ます

/* balance あり: 行の長さが均等になる */
とても長い見出しのテキストが
ここに入ります

昔のやり方

見出しの行バランスを整えるには <br> タグを手動で入れるか、JavaScriptライブラリを使っていました。

<!-- 手動で改行位置を調整していた -->
<h1>とても長い見出しの<br>テキストがここに入ります</h1>

注意点

  • balance は短いテキスト(6行以下)に対して有効です。長い文章には pretty を使いましょう
  • pretty はレンダリングコストがかかるため、パフォーマンスを気にする場面では本文全体への一括適用を避けた方がよい場合もあります

8. @scope

目的

CSSのスタイルを特定のDOMサブツリーに限定して適用します。クラス名の衝突を避けつつ、過度に長いセレクターを書かずに済みます。

できること

/* .article 内の要素にだけスタイルを適用する */
@scope (.article) {
  h2 {
    font-size: 1.5rem;
    border-bottom: 2px solid #333;
  }

  p {
    line-height: 1.8;
  }

  a {
    color: #1a73e8;
    text-decoration: underline;
  }
}

/* .sidebar 内では別のスタイルを適用する */
@scope (.sidebar) {
  h2 {
    font-size: 1.2rem;
    color: #666;
  }

  a {
    color: #e91e63;
  }
}

to キーワードで「ここまで」という下限も指定できます。

/* .article 内だけどfigure の中は除外する */
@scope (.article) to (figure) {
  img {
    border: 2px solid #333;
  }
}

昔のやり方

BEM記法などの命名規則で名前空間を管理するか、CSS Modulesのようなツールに頼っていました。

/* BEMで名前空間を管理 */
.article__title {
  font-size: 1.5rem;
}
.sidebar__title {
  font-size: 1.2rem;
}

注意点

  • 2025年12月にBaseline「Newly Available」になったばかりなので、古いブラウザをサポートする場合は注意が必要です

9. Relative Color Syntax(相対カラー構文)

目的

既存の色を基準にして、明るさ・彩度・透明度などを計算で調整した色を作れます。デザイントークンやテーマカラーの派生色を動的に生成するのに便利です。

できること

:root {
  --brand: #3b82f6;
}

.btn-primary {
  background: var(--brand);
  color: white;
}

/* ホバー時に少し暗くする */
.btn-primary:hover {
  background: oklch(from var(--brand) calc(l * 0.85) c h);
}

/* 背景色として薄い版を作る */
.alert-info {
  background: oklch(from var(--brand) calc(l * 1.3) calc(c * 0.5) h);
  border: 1px solid var(--brand);
}

/* 半透明バージョン */
.overlay {
  background: rgb(from var(--brand) r g b / 0.2);
}

昔のやり方

色の派生バリエーションはすべて手動で計算するか、Sassの lighten()darken() 関数に頼っていました。

// Sassが必要だった
$brand: #3b82f6;

.btn-primary:hover {
  background: darken($brand, 15%);
}

.alert-info {
  background: lighten($brand, 30%);
}

注意点

  • oklchoklab などの色空間を使うと、人間の知覚に近い色操作ができます
  • 2024年9月にBaseline「Newly Available」になった機能です

10. @layer と @scope を活用した Popover API

目的

HTMLの popover 属性とCSS Anchor Positioningを組み合わせて、JavaScriptなしでポップオーバーUIを作れます。ツールチップやドロップダウンメニューの実装が大幅に簡単になります。

できること

<button popovertarget="menu">メニューを開く</button>

<div id="menu" popover>
  <ul>
    <li><a href="/settings">設定</a></li>
    <li><a href="/profile">プロフィール</a></li>
    <li><a href="/logout">ログアウト</a></li>
  </ul>
</div>
[popover] {
  /* 初期状態は非表示(ブラウザが自動で処理) */
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* 表示時のアニメーション */
[popover]:popover-open {
  opacity: 1;
  transform: scale(1);
}

/* 非表示時の初期状態(アニメーション用) */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scale(0.95);
  }
}

popover 属性を付けるだけで、以下の機能が自動で提供されます。

  • クリックで開閉
  • 外側クリックで閉じる
  • Escキーで閉じる
  • 最前面(トップレイヤー)に表示

昔のやり方

JavaScriptで表示・非表示のトグル、外側クリックの検知、キーボードイベントの処理を自前で実装していました。

const btn = document.querySelector('#menu-btn');
const menu = document.querySelector('#menu');

btn.addEventListener('click', () => {
  menu.classList.toggle('open');
});

document.addEventListener('click', (e) => {
  if (!menu.contains(e.target) && e.target !== btn) {
    menu.classList.remove('open');
  }
});

document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    menu.classList.remove('open');
  }
});

注意点

  • CSS Anchor Positioning(anchor-name / position-anchor)と組み合わせるとボタンに対する相対位置指定もできますが、Anchor PositioningはFirefoxでまだフラグが必要なため、本番利用は慎重に判断しましょう
  • popover 属性自体は2024年にBaselineになっており、安心して使えます

まとめ

紹介した10個の機能を振り返りましょう。

機能 できること Baseline状況
CSS Nesting プリプロセッサなしでネスト記法 Widely Available
:has() 親・兄弟の状態でスタイリング Widely Available
Container Queries 親要素幅に応じたレスポンシブ Widely Available
Subgrid 親グリッドのトラック引き継ぎ Widely Available
@layer 詳細度に頼らない優先順位管理 Widely Available
light-dark() ダークモード対応を1行で Newly Available
text-wrap: balance / pretty テキスト折り返しの美しさ制御 Newly Available
@scope DOMサブツリーへのスタイル限定 Newly Available
Relative Color Syntax 色の派生バリエーションを計算 Newly Available
Popover API JSなしでポップオーバーUI Newly Available

「Widely Available」の機能は古いブラウザでも広くサポートされているため、今すぐ本番に導入できます。「Newly Available」の機能も最新ブラウザでは問題なく使えますが、古いブラウザをサポートする場合はフォールバックを検討しましょう。

CSSの進化は本当に速いです。以前はJavaScriptやプリプロセッサに頼っていた多くの機能が、ネイティブCSSだけで実現できるようになりました。少しずつ取り入れて、モダンなCSS開発を楽しんでいきましょう。

Discussion