Zenn
🎨

最近のCSSを改めてちゃんと学んでみた

2025/03/23に公開
3
135

はじめに

弊社ではデザイナーがCSSを書いてくれるので、私はフロントエンドエンジニアながら最近はTypeScriptばかり書いていました。
しかし、フロントエンドエンジニアならCSSもしっかり学んでおかないといけないと思い、アウトプットついでにこの記事を書いています。

1. モダンCSSの現状と進化

2019-2024年に標準化された主要な機能

2019年以降、CSSには多くの革新的な機能が標準化され、JavaScriptに頼らずに実現できることが増えました。

レイアウト関連:

  • コンテナクエリ - 親要素のサイズに基づくレスポンシブデザイン
  • CSS Grid(サブグリッド) - ネストされたグリッドが親グリッドのトラックを継承
  • CSS ネスト - セレクタを入れ子構造で記述(Sassのような書き方)

セレクタと制御:

  • 親要素セレクタ (:has) - 子要素に基づいて親要素を選択
  • カスケードレイヤー (@layer) - スタイルシートの優先順位を明示的に制御
  • スコープ機能 (@scope) - スタイルの適用範囲を明示的に制限

視覚効果とアニメーション:

  • スクロール駆動アニメーション - スクロールに連動したアニメーション
  • color-mix() 関数 - 色を混ぜ合わせて新しい色を作成
  • text-wrap: balance - テキストの行の長さを均等に調整

2024年の新機能

以下の機能は2024年に多くの主要ブラウザでサポートされた最新機能です:

  • field-sizing - フォーム要素が内容に合わせて自動調整
  • interpolate-size - height: auto などへのアニメーション
  • light-dark() 関数 - ライト/ダークモードで異なる色を一度に定義
  • @property - CSSカスタムプロパティに型情報を追加

従来のCSSからの大きな変化点

これらの新機能による主な変化は以下の通りです:

プログラミング的な表現力の向上

CSS変数と計算関数(calc、clamp、min/max)に加え、三角関数(sin、cos、tan)も導入され、より複雑な計算と表現が可能になりました。テーマカラーを一箇所で定義し、派生色を自動計算できるようになりました。

:root {
  --hue: 220; /* 基本の色相 */

  /* 基本色から派生色を計算 */
  --primary: hsl(var(--hue), 80%, 50%);
  --primary-light: hsl(var(--hue), 70%, 70%);
  --primary-dark: hsl(var(--hue), 90%, 30%);
}

レイアウト制御の強化

FlexboxとGridの成熟により、複雑なレイアウトが直感的に実現できるようになりました。特に最新のGridの機能(サブグリッドなど)により、複雑なネストされたレイアウトも簡単に構築できます。

コンポーネント指向への適応

スコープ機能やカスケードレイヤーにより、コンポーネントベースの開発に適したCSS設計が可能になりました。スタイルの衝突や意図しない影響を避けやすくなっています。

変換とアニメーションの精密な制御

個別の変換プロパティ(translate、rotate、scale)が導入され、従来のtransformプロパティよりも直感的に要素の位置や形状を制御できるようになりました。また、ビューのトランジションAPIを使用することで、ページ遷移やDOM変更時のアニメーションを簡単に実現できます。

モダンCSSの設計思想の変化

これらの技術的進化に伴い、CSSの設計思想も変化しています:

  • 分離から統合へ -「HTML・CSS・JSを厳格に分離」から「機能単位でコードをまとめる」コンポーネント指向へ
  • 命令型から宣言型へ -「要素をどう配置するか」から「どんなレイアウトにしたいか」という宣言型アプローチへ
  • ユーザー中心のデザイン - 開発者の利便性より、ユーザー体験(ダークモード対応やアニメーション低減設定など)を優先

2. レイアウト技術の選択と組み合わせ

Flexboxの特性と最適な使用シーン

Flexboxは一次元(行または列の一方向)のレイアウトに特化したモデルです。

適した使用シーン:

  • ナビゲーションメニューなどの横並び要素の均等配置
  • 垂直・水平方向の中央揃え
  • コンテンツ量に応じて自動調整される要素の配置
/* 基本的なFlexboxの例 */
.nav {
  display: flex;
  justify-content: space-between; /* 要素間のスペースを均等に配置 */
  align-items: center; /* 垂直方向中央揃え */
  gap: 1rem; /* 要素間の間隔 */
}

CSS Gridの強みと実践パターン

Gridは二次元(行と列の両方)のレイアウトを制御するモデルです。

適した使用シーン:

  • ページ全体のレイアウト(ヘッダー、サイドバー、メインコンテンツなど)
  • 要素が行や列をまたがる複雑な位置関係
  • 写真ギャラリーなどの規則的なグリッドレイアウト
/* 基本的なGridの例 */
.layout {
  display: grid;
  grid-template-columns: 250px 1fr; /* サイドバー(固定幅)とメインコンテンツ(可変幅) */
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}

.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }

FlexboxとGridの比較

特性 Flexbox Grid
次元 一次元(行または列) 二次元(行と列の両方)
主な用途 要素の配置、整列、間隔設定 複雑なレイアウト構造の定義
計算順序 コンテンツサイズ優先 グリッド構造優先
適したシーン ナビゲーション、カード、小さなUI要素 ページレイアウト、複雑な配置、不規則なグリッド
方向制御 単一方向の制御に強い 行と列の両方を精密に制御可能
自動配置 flex-wrap での簡易な折り返し 自動配置アルゴリズムで複雑な配置が可能
サイズ調整 flex-growflex-shrink で伸縮を制御 分数単位(fr)や minmax() で複雑なサイジング
コードの複雑さ 比較的シンプル より複雑だが表現力が高い

選択基準:

  • Flexbox:一方向の並び、コンテンツサイズ依存の調整が必要な場合
  • Grid:2次元の複雑なレイアウト、整列した構造が必要な場合

複合レイアウトでの効果的な使い分け

実際のプロジェクトでは、両者を組み合わせるのが効果的です。一般的には、ページ全体のレイアウト構造にはGridを使用し、各セクション内の要素の配置にはFlexboxを使うアプローチが取られます。

/* Grid と Flexbox の組み合わせ */
.page {
  display: grid;
  grid-template-columns: 250px 1fr;
  grid-template-rows: auto 1fr auto;
}

.nav {
  display: flex;
  align-items: center;
}

3. CSSのスコープ管理手法

グローバルスコープの課題と対策

CSSはデフォルトでグローバルスコープで動作するため、大規模プロジェクトでは様々な問題が発生します。主な課題は名前衝突、詳細度の戦い、意図しないスタイルの影響(CSS漏れ)などです。

従来はBEMなどの命名規則やCSSモジュールなどで対策してきましたが、最新のCSSではネイティブなスコープ管理機能が導入されました。

カスケードレイヤー(@layer)によるスタイル優先度制御

@layer は、スタイルシートの優先順位を明示的に制御できる新機能です。これによりスタイルの「勝ち負け」を詳細度に頼らず管理できます。

レイヤーは優先順位を宣言でき、後で定義されたレイヤーほど優先度が高くなります。同じ詳細度のルールがある場合、優先度の高いレイヤーのルールが適用されます。

/* レイヤーの宣言と優先順位の設定 */
@layer reset, base, components, utilities;

/* 基本スタイル */
@layer base {
  button {
    padding: 0.5em 1em;
    border-radius: 4px;
  }
}

/* コンポーネントスタイル (baseより優先される) */
@layer components {
  .button-primary {
    background-color: blue;
    color: white;
  }
}

@layerのメリット:

  • 詳細度の問題を解決(!important の乱用防止)
  • 外部ライブラリのCSSとの優先順位を明確に管理
  • 大規模プロジェクトでのCSS管理が容易に

ネイティブスコープ機能(@scope)の活用

@scope は、スタイルの適用範囲を明示的に制限するための新機能です。これにより、特定の要素内だけにスタイルを適用できます。

/* #product-list内の.itemにのみスタイルを適用 */
@scope (#product-list) {
  .item {
    border: 1px solid #ddd;
  }

  /* スコープルート要素自体にスタイルを適用 */
  :scope {
    padding: 1rem;
  }
}

スコープの範囲を指定できます:

/* #comments内から.commentまでの間のp要素にスタイルを適用 */
@scope (#comments) to (.comment) {
  p {
    font-size: 0.9em;
  }
}

追記:@scopeのインラインスタイル構文

@scopeはHTML内の <style> 要素内でも使用できます。この方法では、通常のCSSで必要な @scope (セレクタ) { ... } という書き方と異なり、単に @scope { ... } と書くだけでOKです。

<section class="article-body">
  <style>
    @scope {
      /* ここに書いたスタイルは、自動的にこの<style>タグの親要素
         (この場合はsection.article-body)だけに適用されます */
      img {
        border: 5px solid black;
        background-color: goldenrod;
      }
    }
  </style>

  <!-- セクションの内容 -->
</section>

つまり、上記の例は以下のような通常のCSSを書くのと同じ効果があります:

@scope (.article-body) {
  img {
    border: 5px solid black;
    background-color: goldenrod;
  }
}

インラインスタイル構文の特徴と利点:

  1. 自己完結型コンポーネント:スタイルがコンポーネントのマークアップと共にカプセル化され、コードの関連部分が一箇所にまとまる
  2. スコープの自動設定:親要素が自動的にスコープルートになるため、セレクタを明示的に指定する必要がない
  3. ランタイムスタイル変更:JavaScriptで動的に挿入されるコンポーネントでも、スタイルの衝突なくスコープを維持できる
  4. Shadow DOMの代替:Shadow DOMほど強力な分離ではないが、より軽量な実装でスタイルの分離を実現できる

@layerと@scopeの違い:

  • @layer:スタイルの「優先順位」を制御(どのスタイルが勝つか)
  • @scope:スタイルの「適用範囲」を制御(どこにスタイルが適用されるか)

CSSネストの活用

CSSネストは、セレクタを入れ子構造で記述できる機能で、Sassなどのプリプロセッサなしでもコードの構造をより直感的に表現できます。

/* ネストを使用した記述例 */
.navbar {
  background-color: navy;
  color: white;

  /* 子要素のスタイル */
  .menu-item {
    padding: 10px;
    border-bottom: 1px solid gray;

    /* ホバー状態 (&は親セレクタを参照) */
    &:hover {
      background-color: lightblue;
    }
  }
}

ネストを使うことで、コードの可読性が向上し、コンポーネントの構造をより明確に表現できます。また、メディアクエリやコンテナクエリもネストして記述できるため、関連するスタイルをまとめて管理できます。

4. 変数とカスタムプロパティ

CSS変数の基本と応用テクニック

CSS変数(カスタムプロパティ)は、再利用可能な値を定義する機能です。変数は --(ハイフン2つ)で始まる名前で定義し、var() 関数で使用します。

/* ルートレベルで変数を定義 */
:root {
  --primary-color: #3366ff;
  --spacing-unit: 8px;
}

/* 変数の使用例 */
.button {
  background-color: var(--primary-color);
  padding: calc(var(--spacing-unit) * 2); /* 計算も可能 */
}

CSS変数の主な特徴:

  • スコープと継承 - 変数は要素ごとに再定義でき、その子孫に継承される
  • フォールバック値 - 変数が定義されていない場合の代替値を指定できる (例: var(--color, blue))
  • 動的な計算 - calc() 関数と組み合わせて動的な値を計算できる
  • JavaScript連携 - JS経由で値の取得・設定が可能

CSS変数とSass変数の比較

特性 CSS変数 Sass変数
処理タイミング 実行時(ブラウザ) コンパイル時
動的変更 可能(JS経由や疑似クラスで変更可能) 不可能(生成されたCSSに変数は存在しない)
スコープ CSSのカスケーディングルールに従う レキシカルスコープ(宣言されたブロック内)
DOM依存性 DOM要素の状態に基づいて変化可能 DOM非依存(静的)
継承 親要素から子要素へ自動的に継承される コンパイル時に値が固定される
デバッグ ブラウザの開発ツールで検査・変更可能 コンパイル後のCSSのみデバッグ可能
適した用途 テーマ切り替え、動的スタイル変更 複雑な計算、静的な値、定数

CSS変数とSass変数の使い分けのポイント:

  • CSS変数: テーマ切り替え、ユーザー設定の反映、動的な値の変更
  • Sass変数: コンパイル時に確定する値、大量の計算が必要な値

@property による型付きCSS変数

@property を使うと、CSS変数に型情報を追加できるようになり、アニメーションや補間が可能になります。

/* 色の型付き変数定義 */
@property --myColor {
  syntax: '<color>'; /* 型の指定 */
  inherits: false;  /* 継承の有無 */
  initial-value: hotpink; /* 初期値 */
}

/* 角度の型付き変数定義 */
@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* アニメーション定義 */
@keyframes rotate {
  to {
    --angle: 360deg; /* 型付き変数はアニメーション可能 */
  }
}

/* 実際の使用例 */
.element {
  --angle: 0deg;
  background: linear-gradient(var(--angle), var(--myColor), blue);
  animation: rotate 2s linear infinite;
}

@property の主な利点:

  1. CSS変数に型情報を追加できる(<color>, <length>, <angle> など)
  2. 型付き変数はスムーズにアニメーション・トランジションできる
  3. 初期値を指定でき、変数未定義時のフォールバックとして機能する
  4. 継承動作を制御できる(inherits: true/false

色に関する新機能

color-mix()関数による色の操作

color-mix() は異なる色を混ぜ合わせて新しい色を作成する関数です。

:root {
  --primary: #3366ff;
  /* 白を70%混ぜた明るいバージョン */
  --primary-light: color-mix(in srgb, var(--primary) 30%, white);
  /* 黒を70%混ぜた暗いバージョン */
  --primary-dark: color-mix(in srgb, var(--primary) 30%, black);
  /* オレンジを25%混ぜたアクセントカラー */
  --accent: color-mix(in hsl, var(--primary), orange 25%);
}

color-mix() 関数は様々なカラースペース(srgb、hsl、lch、oklchなど)で動作し、パーセンテージを指定して色の混合比率を調整できます。

light-dark()関数によるテーマ対応色

light-dark() 関数は、現在のカラースキーム(ライトモードかダークモード)に基づいて適切な色を選択する関数です。

:root {
  color-scheme: light dark; /* カラースキームの指定 */

  /* ライトモード用の色, ダークモード用の色 の順に指定 */
  --text-color: light-dark(#333, #fafafa);
  --background-color: light-dark(#e4e4e4, #121212);
  --accent-color: light-dark(hotpink, lime);
}

.button {
  background-color: var(--text-color);
  color: var(--background-color);
}

この例では、ライトモードの場合は最初の値(#333 など)が、ダークモードの場合は2番目の値(#fafafa など)が使用されます。

light-dark()関数のメリット:
複雑な条件分岐やメディアクエリを使わずにテーマ対応のスタイルを作成できるため、コードがシンプルになる。ユーザーのシステム設定変更にも即座に対応する。

テーマ実装とダークモード対応

CSS変数を使ったテーマ切り替え機能の実装例:

/* デフォルト(ライトモード)のカラー設定 */
:root {
  --text-color: #333333;
  --background-color: #ffffff;
}

/* ダークテーマ用のクラス */
.dark-theme {
  --text-color: #eeeeee;
  --background-color: #121212;
}

/* システム設定に基づく自動切り替え */
@media (prefers-color-scheme: dark) {
  :root {
    --text-color: #eeeeee;
    --background-color: #121212;
  }
}

/* 実際の要素に適用 */
body {
  color: var(--text-color);
  background-color: var(--background-color);
}

5. レスポンシブデザインの現代的アプローチ

コンテナクエリの概念と基本的な使い方

コンテナクエリは、親要素のサイズに基づいてスタイルを変更できる革新的な機能です。従来のメディアクエリがビューポート全体のサイズに基づくのに対し、コンテナクエリは特定の親コンテナのサイズに基づきます。

コンテナクエリの使用手順:

  1. コンテナとして振る舞う要素を定義
  2. そのコンテナのサイズに応じたスタイルを記述
/* コンテナの定義 */
.card-container {
  container-type: inline-size; /* インライン方向のサイズに基づくコンテナに指定 */
  container-name: cards; /* 名前付きコンテナ(オプション) */
}

/* 名前付きコンテナを参照 */
@container cards (min-width: 400px) {
  .card {
    display: flex;
    align-items: center;
  }
}

/* 最も近い祖先コンテナを参照(名前なし) */
@container (max-width: 300px) {
  .card-title {
    font-size: 0.9rem;
  }
}

container-type プロパティは、要素をコンテナとして定義し、どの軸(方向)でクエリを使用できるかを指定します:

  • inline-size:インライン方向(横幅)のみを考慮
  • size:インライン方向とブロック方向(高さと幅)の両方を考慮
  • normal:コンテナとして機能しない(デフォルト)

container-name はコンテナに名前を付けます。名前付きコンテナを使うと複数のコンテナが入れ子になっていても特定のコンテナを参照できます。名前を指定しない場合は、最も近い祖先コンテナを参照します。

また、container ショートハンドプロパティで両方をまとめて設定可能です:

.container {
  container: cards / inline-size;  /* 名前と型を一緒に指定 */
}

スタイルクエリの活用

スタイルクエリは、親コンテナのスタイル(CSS変数の値など)に基づいて子要素のスタイルを変更する機能です。

/* テーマに応じたスタイル変更 */
@container style(--theme: dark) {
  .card {
    background-color: #333;
    color: white;
  }
}

/* 重要度に応じたスタイル変更 */
@container style(--importance: high) {
  .alert {
    border-width: 2px;
    font-weight: bold;
  }
}

この例では、親コンテナに style="--theme: dark;"style="--importance: high;" が設定されていると、子要素のスタイルが変更されます。

メディアクエリとコンテナクエリの使い分け

メディアクエリの適した用途:

  • ページ全体のレイアウト変更
  • グローバルなスタイル調整
  • デバイスの特性に応じた調整
@media (max-width: 768px) {
  .layout {
    grid-template-columns: 1fr; /* 1列レイアウトに変更 */
  }
}

コンテナクエリの適した用途:

  • 再利用可能なコンポーネントの適応
  • 配置場所に応じたレイアウト調整
  • 親要素のサイズに基づく調整
@container (min-width: 400px) {
  .card {
    flex-direction: row; /* 横向きレイアウトに変更 */
  }
}

ベストプラクティス:
メディアクエリでページ全体のレイアウトを制御し、コンテナクエリで個々のコンポーネントの内部レイアウトを調整するという組み合わせが効果的です。

最新のビューポート単位と計算関数

最新のCSSでは新しいビューポート単位や計算関数が導入され、より柔軟なレスポンシブデザインが可能になりました。

モバイルブラウザのUI要素に対応する新しいビューポート単位:

  • svh/svw:Small Viewport - UI要素が表示されている状態の最小ビューポート
  • lvh/lvw:Large Viewport - UI要素が隠れている状態の最大ビューポート
  • dvh/dvw:Dynamic Viewport - UI要素の表示/非表示に動的に対応
/* モバイルUIに対応した高さ指定 */
.hero {
  height: 100vh; /* 従来のビューポート高さ */
  height: 100dvh; /* 動的なビューポート高さ - モバイルUIに対応 */
}

値の範囲を制御する計算関数:

/* 90%か1200pxの小さい方 */
width: min(90%, 1200px);

/* 16px〜24pxの範囲で可変 */
font-size: clamp(1rem, 1rem + 1vw, 1.5rem);

/* 複合的な計算 */
padding: calc(10px + max(1vw, 0.5rem));

text-wrap: balance によるテキスト改善

text-wrap: balance プロパティは、テキストの各行の長さを均等に調整し、特に見出しなどの短いテキストブロックで視覚的なバランスを改善します。

/* 見出しなどに適用する */
h1, h2, h3, .caption {
  text-wrap: balance;
}

適応型レイアウトの設計パターン

自動調整グリッド(子要素が自動的に適切な列数で配置):

.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

コンテンツベースのサイジング(要素のサイズをその内容に基づいて設定):

/* コンテンツに合わせたサイズ設定 */
.flexible-width {
  width: fit-content;
  max-width: 100%;
}

.content-based {
  width: min-content;
}

6. モダンなセレクタと疑似クラス

親要素セレクタ(:has)の革新的な使い方

:has() セレクタは、長年CSSに欠けていた「親セレクタ」機能を提供します。これにより、子要素の状態に基づいて親要素を選択できるようになりました。

基本的な使用例:

/* 画像を含む段落を選択 */
p:has(img) {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* チェックされた入力を持つラベルを選択 */
label:has(input:checked) {
  font-weight: bold;
  color: blue;
}

実践的な使用例:

/* 子要素が3つ以上ある場合にグリッドレイアウトに変更 */
.container:has(> *:nth-child(3)) {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

/* 無効な入力がある場合にフォームグループをハイライト */
.form-group:has(input:invalid) {
  background-color: #fff0f0;
  border-left: 3px solid red;
}

/* モーダルが開いている時に背景スクロールを防止 */
html:has(dialog[open]) {
  overflow: hidden;
}

:nth-child()の「of S」構文

:nth-child() セレクタの「of S」構文は、特定のセレクタにマッチする子要素の中から特定の順番の要素を選択する機能です。

/* .item クラスを持つ要素のうち、3番目の要素を選択 */
.container :nth-child(3 of .item) {
  color: red;
}

/* 「スポーツ」カテゴリーの記事のうち、3番目の記事にスタイルを適用 */
.news-list :nth-child(3 of .sports) {
  font-weight: bold;
  border-left: 3px solid blue;
}

この構文を使用することで、特定のタイプやクラスの要素だけをカウントして順番を指定できるため、複雑なリストやグリッドレイアウトでの選択がより直感的になります。

機能的なセレクタ(:is、:where、:not)

これらのセレクタは複雑なセレクタパターンをシンプルに記述するための機能です。

:is()セレクタ(複数のセレクタをグループ化):

/* 3つの要素内のaタグのホバー状態を一度に指定 */
:is(header, main, footer) a:hover {
  text-decoration: underline;
}

:where()セレクタ(詳細度が0になるグループ化):

/* 詳細度が低いベースのスタイル */
:where(header, main, footer) a {
  color: blue;
}

:not()セレクタ(複数のセレクタを除外):

/* 特定のクラスを持たないリンクにスタイル適用 */
a:not(.primary, .secondary, .tertiary) {
  color: gray;
}
:is() と :where() の重要な違い
  • :is() はグループ内で最も高い詳細度を継承する
  • :where() は詳細度が常に0なので、後から簡単に上書き可能である

7. フォームとバリデーション

フォーム関連の疑似クラスと機能

フォーム検証の疑似クラス

入力検証に関する疑似クラスを使うと、フォームのスタイリングをより細かく制御できます。

/* 有効な入力のスタイル */
input:valid {
  border-color: green;
}

/* 無効な入力のスタイル(ユーザーが操作した後にのみ適用) */
input:user-invalid {
  background-color: #fff0f0;
  border-color: red;
}

主な検証疑似クラス:

  • :valid - 入力値が有効な場合
  • :invalid - 入力値が無効な場合
  • :user-invalid - ユーザーが操作した後で入力値が無効な場合(初期状態ではエラー表示しない)

フォーカス状態の詳細制御

/* キーボードナビゲーションなどのフォーカス時のスタイル */
:focus-visible {
  outline: 2px solid blue;
  outline-offset: 2px;
}

/* マウスクリックでのフォーカス時はアウトラインを非表示に */
:focus:not(:focus-visible) {
  outline: none;
}

field-sizing プロパティ

field-sizing プロパティを使用すると、テキストエリアやインプット要素が内容に応じて自動的にサイズ調整されるようになります。

textarea, select, input {
  field-sizing: content; /* 内容に合わせてサイズ調整 */
}

アクセシビリティを考慮したフォーカス管理

アクセシビリティに配慮したフォーカス管理は、現代のウェブ開発において非常に重要です。

/* キーボードナビゲーションでのフォーカス状態をはっきりと表示 */
:focus-visible {
  outline: 3px solid #4d90fe;
  outline-offset: 2px;
}

/* アニメーション低減設定に対応 */
@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
  }
}

アクセシビリティのポイント

  • キーボード操作時には明確なフォーカス表示が必要
  • マウス操作時にはより控えめな表示が適している
  • ユーザーの設定(アニメーション低減など)を尊重する

8. アニメーションとトランジション

interpolate-size プロパティ

従来のCSSでは、width: autoheight: auto などの値へのアニメーションは不可能でしたが、interpolate-size プロパティによりこれが可能になりました。

/* ページ全体で有効化する場合 */
:root {
  interpolate-size: allow-keywords;
}

/* 特定の要素に対してのみ有効化する場合 */
.expandable {
  interpolate-size: allow-keywords;
  transition: width 0.3s, height 0.3s;
}

.expandable:hover {
  width: auto; /* auto値へのアニメーションが可能に */
  height: auto;
}

より細かい制御が必要な場合は、calc-size() 関数を使用できます:

.accordion {
  transition: height 0.5s;
  height: 50px;
}

.accordion.expanded {
  height: calc-size(auto); /* auto値を計算してアニメーション */
}

interpolate-size の活用シーン:

  • アコーディオンメニューのスムーズな開閉
  • フィードバックフォームの動的な高さ調整
  • 可変サイズのコンテンツコンテナのアニメーション

スクロール駆動アニメーション

スクロール駆動アニメーションは、スクロール位置に連動してアニメーションを実行できる機能です。この機能により、ユーザーがページをスクロールする際に要素をアニメーション化する効果を、JavaScriptを使わずにCSSだけで実現できます。

/* アニメーションの定義 */
@keyframes reveal {
  from { opacity: 0; transform: translateY(50px); }
  to { opacity: 1; transform: translateY(0); }
}

/* スクロールに連動するアニメーション */
.scroll-reveal {
  opacity: 0;
  animation: reveal 1s linear forwards;
  animation-timeline: scroll(); /* スクロール位置をタイムラインとして指定 */
  animation-range: entry 10% cover 50%; /* 開始と終了タイミングを指定 */
}

ここでの重要なプロパティは以下の2つです:

  • animation-timeline: scroll();:アニメーションの進行をスクロール位置に連動させる。これによって、通常の時間ベースのアニメーションではなく、スクロール位置を基準にしたアニメーションになる。

  • animation-range: entry 10% cover 50%;:アニメーションの開始と終了タイミングを指定する。この例では:

    • entry 10%:要素が表示領域に10%入った時点でアニメーションを開始
    • cover 50%:要素が表示領域の50%をカバーした時点でアニメーションを完了

他にも、exit(要素が表示領域から出始める)や contain(要素全体が表示領域に含まれる)などの値を使用できる。

プログレスバーの実装例:

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: linear-gradient(90deg, #3498db, #8e44ad);
  transform-origin: left;
  animation: grow-width 1s linear forwards;
  animation-timeline: scroll(root); /* ページ全体のスクロールを参照 */
}

@keyframes grow-width {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

scroll(root) を指定することで、ページ全体のスクロール位置を参照し、それに応じてプログレスバーが伸びるアニメーションを実現できる。

スクロールスナップのイベント

スクロールスナップ機能にJavaScriptイベントが追加され、より洗練されたインタラクションが可能になりました。

const scroller = document.querySelector('.snap-container');

// スナップが完了したときに発火
scroller.addEventListener('scrollsnapchange', event => {
  console.log('Snapped to:', event.snapTargetBlock, event.snapTargetInline);
  updateUI(event.snapTargetBlock);
});

// スナップ中に発火(進行中のスナップを検知)
scroller.addEventListener('scrollsnapchanging', event => {
  showVisualFeedback(event.snapTargetBlock);
});

スクロールスナップイベントの活用シーン:

  • カルーセルのページネーション表示
  • スクロールベースのナビゲーション
  • スクロールによる要素選択UI

個別の変換プロパティ

従来のCSSでは、要素の変換(移動、回転、拡大縮小)にtransformプロパティを使用していましたが、個別の変換プロパティを使うことで、より直感的に要素を操作できるようになりました。

/* 従来の方法 */
.element {
  transform: translateX(50px) rotate(45deg) scale(1.2);
}

/* 新しい方法 */
.element {
  translate: 50px 0; /* X軸方向に50px移動 */
  rotate: 45deg;     /* 45度回転 */
  scale: 1.2;        /* 1.2倍に拡大 */
}

独立したアニメーションの例:

.card {
  translate: 0 0;
  rotate: 0deg;
  scale: 1;
  transition: translate 0.3s, rotate 0.5s, scale 0.3s; /* 異なるタイミングで変化 */
}

.card:hover {
  translate: 0 -10px; /* 上に10px浮き上がる */
  rotate: 5deg;       /* 5度傾く */
  scale: 1.05;        /* 5%拡大 */
}

個別の変換プロパティのメリット:

  1. 独立したアニメーションが容易 - 1つの変換だけを変更・アニメーションできる
  2. コードの可読性が向上 - 適用されている変換が明確に分かる
  3. 一貫した適用順序 - translateの後にrotate、最後にscaleという順序で常に適用される

ビューのトランジション

View Transition APIは、ページ間やDOM要素の変更時にスムーズなアニメーション効果を提供する新機能です。

同一ドキュメント内のトランジション

// ビュー遷移の基本的な使い方
function updateUI() {
  // APIをサポートしていないブラウザのフォールバック
  if (!document.startViewTransition) {
    updateDOM();
    return;
  }

  // ビュー遷移を開始
  document.startViewTransition(() => {
    updateDOM(); // DOM更新処理
  });
}

クロスドキュメントトランジション

異なるページ間でのビュー遷移(クロスドキュメントトランジション)がサポートされました。

/* 自動的にページ遷移アニメーションを適用 */
@view-transition {
  navigation: auto;
}

JavaScriptを使って明示的にページ間トランジションを制御できます:

// 別のページへのナビゲーション時にトランジションを適用
function navigateTo(url) {
  if (!document.startViewTransition) {
    window.location.href = url;
    return;
  }

  document.startViewTransition(async () => {
    window.location.href = url;
  });
}

トランジションのカスタマイズ

/* 古いページのフェードアウト */
::view-transition-old(root) {
  animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both fade-out;
}

/* 新しいページのフェードイン */
::view-transition-new(root) {
  animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both fade-in;
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

9. UIコンポーネントの拡張機能

アンカー配置(Anchor Positioning)

CSS Anchor Positioningは、ポップオーバーやツールチップなどの要素を別の要素(アンカー)に対して正確に配置するための新機能です。

アンカー関係の設定

/* アンカー要素の定義 */
.button {
  anchor-name: --tooltip-anchor;
}

/* 配置する要素 */
.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor; /* アンカーを指定 */
  top: anchor(--tooltip-anchor bottom); /* アンカーのbottomに合わせる */
  left: anchor(--tooltip-anchor center); /* アンカーの中央に合わせる */
}

フォールバック位置の指定

要素がビューポートからはみ出す場合に備えた代替配置位置を指定可能:

@position-fallback --tooltip-positions {
  /* 最初に試す位置 - アンカーの上部中央 */
  @try {
    bottom: calc(anchor(--tooltip-anchor top) - 8px);
    left: anchor(--tooltip-anchor center);
  }

  /* 次に試す位置 - アンカーの下部中央 */
  @try {
    top: calc(anchor(--tooltip-anchor bottom) + 8px);
    left: anchor(--tooltip-anchor center);
  }

  /* 最後に試す位置 - アンカーの左側中央 */
  @try {
    right: calc(anchor(--tooltip-anchor left) - 8px);
    top: anchor(--tooltip-anchor center);
  }
}

.tooltip {
  position-fallback: --tooltip-positions;
}

CSS Anchor Positioningのメリット:

  1. 要素間の配置関係をJavaScriptなしで宣言的に定義
  2. ビューポートのエッジを自動検出し、最適な位置を選択
  3. 論理的な配置プロパティをサポート(RTLなどの方向性に対応)
  4. スクロールやリサイズ時に位置が自動的に更新

initial-letter による装飾的な文字表現

initial-letter プロパティを使用すると、段落の最初の文字を大きく表示する「ドロップキャップ」スタイルを簡単に実現できます。

/* 段落の最初の文字を3行分の高さにする */
p::first-letter {
  initial-letter: 3;
  color: #3366ff;
  font-weight: bold;
  margin-right: 0.5em;
}

サイズと沈み込みの深さを指定する例:

/* 3行分の高さで、2行分沈み込む */
article p:first-of-type::first-letter {
  initial-letter: 3 2;
  font-family: serif;
  font-weight: bold;
  color: #3366ff;
  margin-right: 0.3em;
}

paint-order プロパティ

paint-order プロパティは、テキストの塗りつぶしとストローク(輪郭線)の描画順序を制御できる機能です。

h1 {
  /* ストロークを先に描画し、その上に塗りつぶしを重ねる */
  paint-order: stroke fill;

  color: white;
  -webkit-text-stroke: 5px black;
}

SVG要素への適用例:

svg text {
  paint-order: stroke fill markers;
  stroke: #000;
  stroke-width: 5px;
  fill: #fff;
}

ポップオーバーAPIの活用

ポップオーバーAPIは、ダイアログ、ツールチップ、ドロップダウンメニューなどの要素を簡単に作成するためのネイティブなソリューションを提供します。

<button popovertarget="info-popup">詳細を表示</button>
<div id="info-popup" popover>
  <h3>詳細情報</h3>
  <p>これはポップオーバーの内容です。</p>
  <button popovertarget="info-popup" popovertargetaction="hide">閉じる</button>
</div>

ポップオーバーのスタイルとアニメーション

[popover] {
  padding: 1em;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

[popover]:popover-open {
  animation: fade-in 0.3s ease-out;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

@starting-style の活用

@starting-style を使用すると、ポップオーバーの表示時に初期スタイルを指定して、スムーズなアニメーションを実現できます:

[popover]:popover-open {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s, transform 0.3s;

  /* 初期状態(トランジション開始時)のスタイル */
  @starting-style {
    opacity: 0;
    transform: translateY(-20px);
  }
}
popover APIの便利な特徴
  • 「トップレイヤー」に表示され、z-indexを気にする必要がない
  • クリックした場所の外側をクリックすると自動的に閉じる
  • ESCキーで閉じることができる
  • キーボードによるアクセシビリティがネイティブでサポートされている

<selectmenu>要素のカスタマイズ

<selectmenu> は、従来の <select> 要素の代替として導入された新しい要素で、完全にカスタマイズ可能なドロップダウンメニューを作成できます。

<selectmenu class="custom-select">
  <button slot="button" class="select-button">
    <span>選択してください</span>
    <span slot="selected-value"></span>
    <span class="icon"></span>
  </button>
  <option value="option1">オプション1</option>
  <option value="option2">オプション2</option>
  <option value="option3">オプション3</option>
</selectmenu>

セレクトメニューのスタイリング

.custom-select {
  width: 100%;
  max-width: 300px;
}

.select-button {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 0.5em 1em;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: white;
}

/* ドロップダウンリスト部分のスタイル */
.custom-select::part(listbox) {
  border: 1px solid #eee;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* オプション項目のスタイル */
.custom-select option {
  padding: 0.5em 1em;
}

.custom-select option:hover {
  background-color: #f5f5f5;
}

details要素の拡張機能

name 属性を使用することで、排他的なアコーディオン(1つを開くと他が閉じる)を簡単に実装できるようになりました。

<details name="learn-css">
  <summary>CSSの基本</summary>
  <p>CSSはCascading Style Sheetsの略で...</p>
</details>

<details name="learn-css">
  <summary>セレクタとプロパティ</summary>
  <p>CSSセレクタは要素を選択するための...</p>
</details>

<details name="learn-css">
  <summary>レイアウト技術</summary>
  <p>モダンCSSではFlexboxやGridを使って...</p>
</details>

水平アコーディオンのスタイリング

display プロパティや ::details-content 疑似要素を使用して、より高度なレイアウトやスタイリングが可能になりました:

details {
  display: flex;
  flex-direction: row;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-bottom: 8px;
}

summary {
  flex: 0 0 200px;
  font-weight: bold;
  padding: 8px;
}

details::details-content {
  padding: 8px;
  border-left: 1px solid #ccc;
}

まとめ

モダンCSSは強力な機能セットを提供し、複雑なレイアウトとインタラクションを純粋なCSSで実現できるようになりました。
これらの機能を適切に組み合わせることで、メンテナンス性が高く、パフォーマンスに優れ、アクセシビリティに配慮したウェブサイトを構築できます。JavaScriptに頼らずとも、純粋なCSSで多くの動的な表現が可能になりました。
CSSはもはやプログラミング言語のような存在になりつつあります。

GitHubで編集を提案
135
GMOメディアテックブログ

Discussion

junerjuner

あれ、 @scope の第二構文とかは解説しないのでしょうか……?

HTML で <style> 要素の中に記載するインラインスタイルとして。この場合、前置き部は除外され、囲まれたルールセットは自動的に <style> 要素の親要素のスコープになります。

<parent-element>
  <style>
    @scope {
      ルールセット
    }
  </style>
</parent-element>

https://developer.mozilla.org/ja/docs/Web/CSS/@scope

まぁ、これを元に issues として タグの style 属性に css nesting を許容したいみたいなのもありますが。(これはまだ仕様に含まれることになるかは不明。時間が足りない

https://github.com/w3c/csswg-drafts/issues/8752

Riya AmemiyaRiya Amemiya

ご指摘いただきありがとうございます!
おっしゃる通り、@scopeの第二構文(インラインスタイルでの使用方法)について解説が漏れていました。
貴重なご意見ありがとうございます!
記事に追記しておきます🙏

ログインするとコメントできます