CSS @scopeルールで実現するスコープ付きスタイル完全ガイド


CSS @scopeルールで実現するスコープ付きスタイル完全ガイド

CSSのスコープ管理は、長年の課題でした。BEMやCSS Modules、CSS-in-JSなど、様々な手法が考案されてきましたが、CSS標準仕様として@scopeルールが導入されることで、ネイティブなスコープ管理が可能になります。

@scopeルールとは

@scopeは、CSSスタイルの適用範囲を明示的に制限できる新しいアットルールです。特定のDOM要素の範囲内でのみスタイルを適用することで、スタイルの衝突を防ぎ、より保守性の高いCSSを書くことができます。

主な特徴

  • 明示的なスコープ: スタイルの適用範囲を明確に定義
  • 境界の設定: 上限(root)と下限(limit)を指定可能
  • 詳細度の制御: スコープ内のスタイルは自然に優先される
  • ネイティブサポート: ビルドツール不要で動作

基本的な構文

最もシンプルな@scopeの使用例から始めましょう。

@scope (.card) {
  h2 {
    color: blue;
    font-size: 1.5rem;
  }

  p {
    line-height: 1.6;
  }

  button {
    background: blue;
    color: white;
  }
}

このコードは、.cardクラスを持つ要素内のみでh2pbuttonにスタイルを適用します。

<div class="card">
  <h2>カード内の見出し</h2> <!-- 青色になる -->
  <p>カード内のテキスト</p>
  <button>ボタン</button> <!-- 青背景になる -->
</div>

<h2>カード外の見出し</h2> <!-- スタイルは適用されない -->
<button>外部ボタン</button> <!-- スタイルは適用されない -->

スコープルート(Scope Root)

@scopeの第一引数がスコープルートです。この要素の子孫にのみスタイルが適用されます。

/* 特定のIDをスコープルートに */
@scope (#main-content) {
  article {
    max-width: 800px;
    margin: 0 auto;
  }
}

/* 複数のセレクタも可能 */
@scope (.card, .panel, .widget) {
  header {
    border-bottom: 2px solid #ccc;
  }
}

/* 属性セレクタも使用可能 */
@scope ([data-theme="dark"]) {
  body {
    background: #1a1a1a;
    color: #ffffff;
  }
}

スコープリミット(Scope Limit)

@scopeの第二引数として、スコープの下限を設定できます。この境界内の要素には、スタイルが適用されません。

@scope (.article) to (.nested-article) {
  /* .article内だが、.nested-article内は除外 */
  h1 {
    font-size: 2rem;
    color: navy;
  }

  p {
    margin: 1em 0;
  }
}

HTMLでの動作例:

<article class="article">
  <h1>メイン記事の見出し</h1> <!-- スタイル適用 -->
  <p>メイン記事の本文</p> <!-- スタイル適用 -->

  <article class="nested-article">
    <h1>入れ子記事の見出し</h1> <!-- スタイル適用されない -->
    <p>入れ子記事の本文</p> <!-- スタイル適用されない -->
  </article>

  <p>メイン記事の続き</p> <!-- スタイル適用 -->
</article>

実践例1: カードコンポーネント

カードコンポーネントのスタイルをスコープ化してみましょう。

@scope (.card) {
  /* カード全体 */
  :scope {
    display: flex;
    flex-direction: column;
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  /* カードヘッダー */
  .header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 1rem;
  }

  .header h3 {
    margin: 0;
    font-size: 1.25rem;
  }

  /* カードボディ */
  .body {
    padding: 1rem;
    flex: 1;
  }

  .body p {
    margin: 0.5em 0;
    color: #333;
  }

  /* カードフッター */
  .footer {
    background: #f5f5f5;
    padding: 0.75rem 1rem;
    border-top: 1px solid #ddd;
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
  }

  .footer button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
  }

  .footer .primary {
    background: #667eea;
    color: white;
  }

  .footer .secondary {
    background: transparent;
    color: #667eea;
    border: 1px solid #667eea;
  }
}

このスタイルを使用するHTML:

<div class="card">
  <div class="header">
    <h3>カードタイトル</h3>
  </div>
  <div class="body">
    <p>カードの本文コンテンツがここに入ります。</p>
    <p>複数の段落も問題なく表示されます。</p>
  </div>
  <div class="footer">
    <button class="secondary">キャンセル</button>
    <button class="primary">確定</button>
  </div>
</div>

実践例2: テーマシステム

異なるテーマで同じコンポーネントのスタイルを変える例です。

/* ライトテーマ */
@scope ([data-theme="light"]) {
  .dashboard {
    background: #ffffff;
    color: #1a1a1a;
  }

  .sidebar {
    background: #f5f5f5;
    border-right: 1px solid #ddd;
  }

  .button {
    background: #007bff;
    color: white;
  }

  .button:hover {
    background: #0056b3;
  }

  .input {
    background: white;
    border: 1px solid #ccc;
    color: #1a1a1a;
  }
}

/* ダークテーマ */
@scope ([data-theme="dark"]) {
  .dashboard {
    background: #1a1a1a;
    color: #ffffff;
  }

  .sidebar {
    background: #2a2a2a;
    border-right: 1px solid #444;
  }

  .button {
    background: #0d6efd;
    color: white;
  }

  .button:hover {
    background: #0a58ca;
  }

  .input {
    background: #2a2a2a;
    border: 1px solid #444;
    color: #ffffff;
  }
}

/* ハイコントラストテーマ */
@scope ([data-theme="high-contrast"]) {
  .dashboard {
    background: #000000;
    color: #ffffff;
  }

  .sidebar {
    background: #000000;
    border-right: 3px solid #ffffff;
  }

  .button {
    background: #ffffff;
    color: #000000;
    border: 2px solid #ffffff;
  }

  .button:hover {
    background: #000000;
    color: #ffffff;
  }

  .input {
    background: #000000;
    border: 2px solid #ffffff;
    color: #ffffff;
  }
}

JavaScriptでテーマを切り替え:

function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('theme', theme);
}

// 初期化
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);

// テーマスイッチャー
document.getElementById('theme-switcher').addEventListener('change', (e) => {
  setTheme(e.target.value);
});

実践例3: ネストされたコンポーネント

親コンポーネントと子コンポーネントでスタイルを分離する例です。

/* 親コンポーネント(フォーム全体) */
@scope (.form-container) to (.nested-form) {
  :scope {
    max-width: 600px;
    margin: 2rem auto;
    padding: 2rem;
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  }

  h2 {
    margin-top: 0;
    color: #333;
  }

  .form-group {
    margin-bottom: 1.5rem;
  }

  label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: #555;
  }

  input, textarea, select {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }

  button[type="submit"] {
    width: 100%;
    padding: 1rem;
    background: #28a745;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    cursor: pointer;
  }
}

/* 子コンポーネント(入れ子のフォーム) */
@scope (.nested-form) {
  :scope {
    background: #f8f9fa;
    padding: 1rem;
    border-radius: 4px;
    margin: 1rem 0;
  }

  h3 {
    margin-top: 0;
    font-size: 1rem;
    color: #666;
  }

  .form-group {
    margin-bottom: 1rem;
  }

  input {
    background: white;
    font-size: 0.875rem;
  }

  button {
    padding: 0.5rem 1rem;
    background: #6c757d;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 0.875rem;
  }
}

HTMLでの使用例:

<div class="form-container">
  <h2>メインフォーム</h2>

  <div class="form-group">
    <label>名前</label>
    <input type="text" placeholder="山田太郎">
  </div>

  <div class="form-group">
    <label>メールアドレス</label>
    <input type="email" placeholder="email@example.com">
  </div>

  <!-- ネストされたフォーム -->
  <div class="nested-form">
    <h3>追加情報(任意)</h3>

    <div class="form-group">
      <label>電話番号</label>
      <input type="tel">
    </div>

    <div class="form-group">
      <label>住所</label>
      <input type="text">
    </div>
  </div>

  <button type="submit">送信</button>
</div>

:scopeセレクタ

@scopeブロック内で:scopeセレクタを使用すると、スコープルート自体を参照できます。

@scope (.widget) {
  /* スコープルート自体 */
  :scope {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
    padding: 1rem;
    background: white;
    border: 1px solid #ddd;
  }

  /* スコープルート直下の子要素 */
  :scope > .item {
    padding: 1rem;
    background: #f5f5f5;
  }

  /* 特定の状態のスコープルート */
  :scope:hover {
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  }

  :scope.active {
    border-color: #007bff;
  }
}

詳細度(Specificity)の扱い

@scope内のセレクタは、スコープの文脈を持つため、自然に優先されます。

/* グローバルスタイル */
button {
  background: gray;
  color: white;
}

/* スコープ付きスタイル */
@scope (.special-section) {
  button {
    background: blue; /* こちらが優先される */
  }
}

ただし、明示的な詳細度は通常通り機能します:

@scope (.container) {
  button {
    background: blue;
  }

  .primary-button {
    background: green; /* より高い詳細度 */
  }

  #special {
    background: red; /* さらに高い詳細度 */
  }
}

メディアクエリとの組み合わせ

@scope@mediaクエリと組み合わせて使用できます。

@scope (.responsive-grid) {
  :scope {
    display: grid;
    gap: 1rem;
  }

  @media (max-width: 768px) {
    :scope {
      grid-template-columns: 1fr;
    }

    .item {
      padding: 0.5rem;
    }
  }

  @media (min-width: 769px) {
    :scope {
      grid-template-columns: repeat(3, 1fr);
    }

    .item {
      padding: 1rem;
    }
  }

  @media (min-width: 1200px) {
    :scope {
      grid-template-columns: repeat(4, 1fr);
    }
  }
}

または、@scope@media内にネストすることも可能:

@media (prefers-color-scheme: dark) {
  @scope (.card) {
    :scope {
      background: #1a1a1a;
      color: white;
    }

    .header {
      background: #2a2a2a;
    }
  }
}

コンテナクエリとの組み合わせ

CSS Container Queriesとも組み合わせられます。

@scope (.product-card) {
  :scope {
    container-type: inline-size;
    container-name: card;
  }

  .image {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
  }

  .title {
    font-size: 1rem;
  }

  .description {
    font-size: 0.875rem;
  }

  @container card (min-width: 400px) {
    :scope {
      display: grid;
      grid-template-columns: 200px 1fr;
    }

    .image {
      width: 200px;
    }

    .title {
      font-size: 1.5rem;
    }

    .description {
      font-size: 1rem;
    }
  }
}

ブラウザサポートと代替案

@scopeは比較的新しい機能のため、ブラウザサポートを確認する必要があります。

フィーチャー検出

@supports (selector(:scope)) {
  @scope (.modern-component) {
    /* @scopeをサポートするブラウザ用 */
    .title {
      color: blue;
    }
  }
}

@supports not (selector(:scope)) {
  /* フォールバック */
  .modern-component .title {
    color: blue;
  }
}

PostCSSプラグインでのポリフィル

postcss-scopeプラグインを使用すると、ビルド時に@scopeを変換できます。

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-scope')(),
    require('autoprefixer')
  ]
};

まとめ

CSS @scopeルールは、スタイルのカプセル化とスコープ管理に革新をもたらします。BEMやCSS Modulesのような命名規則に頼らず、ネイティブCSSでスタイルの適用範囲を制御できるため、よりシンプルで保守性の高いコードを書くことができます。

コンポーネントベースの開発において、@scopeは強力なツールとなるでしょう。既存のプロジェクトへの段階的な導入も可能なので、ぜひ試してみてください。