CSS講座 / CSSの整理:共通スタイル・ネスト・カスケードレイヤー

保守しやすいCSSは、共通値、文書の基本、配置、部品、条件付き調整の役割を整理します。ネイティブネストは関連するセレクターをまとめ、カスケードレイヤーはルール群の優先関係を明示します。

重複とファイルの責任を整理する

同じ色・余白、使わないセレクター、似たボタンの指定を探します。同じ役割の値は変数へ、共通の部品は基本と必要な差分へ分けます。ただし重複をすべて消すために複雑な条件を作るより、独立した二部品の方が扱いやすい場合もあります。

/* Before cleanup: Same value repeated in multiple places */
.lesson-button { padding: .65rem 1rem; color: #166b92; }
.notice-button { padding: .65rem 1rem; color: #166b92; }

/* After organizing: sharing common shapes and roles */
.button { padding: .65rem 1rem; color: var(--accent); }
.button.text-alert { color: #9d2632; }

これは整理方針の部分例です。共通クラスは実際のHTMLにも付けます。CSSの名前だけ変えても対応しません。tokens.cssは共有値、base.cssは文書とフォーカス、layout.cssは配置、components.cssは部品と状態、utilities.cssは明示的な小さな補助を担当させます。

小さい文書なら一ファイルをコメントで区切るだけでも十分です。分けただけで衝突がなくなるわけではなく、パスとカスケードも管理します。変数の範囲と継承を参照してください。

ネストの親自身と子孫を区別する

ネイティブCSSネストはブラウザーが直接解釈します。Sassなどがビルド時に別のCSSを生成する方式とは区別します。

.card {
  & h2 { color: var(--accent); }
  &:hover { border-color: var(--accent); }
}

& h2はカード内のh2、&:hoverはカード自身のホバーです。& :hoverと空白を入れるとホバーされた子孫になり、&.warningと& .warningも同じ要素と子孫の違いがあります。

/* The simple overlap above is expanded */
.card h2 { color: var(--accent); }
.card:hover { border-color: var(--accent); }

単一の親なら展開後のルールと比べやすくなります。ただしカンマで複数の親を列挙したネストでは、最も高い詳細度が影響する場合があり、単純な文字列連結ではありません。深すぎる入れ子を避け、未対応環境が必要なら展開したCSSや適切なビルドを選びます。MDNのネスト、ネストの詳細度を参照してください。

詳細度の前にレイヤーを比較する

同じ作者の通常宣言なら、レイヤー外がレイヤー内より優先されます。レイヤー内は、先に登録した順序の後のものが優先です。そのため後のレイヤーのクラスが前のレイヤーのIDに勝つ場合があります。

@layer tokens, base, layout, components, utilities;
@layer base { #layer-target { color: #183044; } }
@layer utilities { .text-alert { color: #9d2632; } }

このボタンは両セレクターに一致し、通常宣言では後のutilitiesの赤になります。同じ名前のレイヤーを後で開き直しても、そのレイヤー自体の順番は移動しません。

importantではレイヤーの順が逆になり、前のレイヤーが後より、レイヤー内が外より優先されます。これは同じ作者のスタイルでレイヤーを比べる説明であり、ユーザースタイル、インライン、動作中の遷移なども含む全条件をレイヤーだけで判断するものではありません。MDNの@layer、カスケードを参照してください。

HTMLと五つのCSSファイルを作る

空のフォルダーに次の構成を作り、すべてUTF-8で保存します。index.htmlのstyles/...は下のサブフォルダーを指します。外部ライブラリーやサーバーは不要です。

stylesheet-project/
  index.html
  styles/
    tokens.css
    base.css
    layout.css
    components.css
    utilities.css

index.htmlにはリンクタグと実際のクラスがあります。比較ボタンは処理を実行するものではなく、色とフォーカスを観察するためのものです。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CSS files and layers by responsibility</title>
  <link rel="stylesheet" href="styles/tokens.css">
  <link rel="stylesheet" href="styles/base.css">
  <link rel="stylesheet" href="styles/layout.css">
  <link rel="stylesheet" href="styles/components.css">
  <link rel="stylesheet" href="styles/utilities.css">
</head>
<body>
  <main class="demo">
    <h1>Organizing a small stylesheet</h1>
    <article class="card">
      <h2>Scope nested rules to the card</h2>
      <p>The card heading uses the accent color. Compare it with the heading outside the card.</p>
      <button class="button" type="button">Regular button</button>
    </article>
    <section><h2>Heading outside the card</h2><p>Use the default font color.</p></section>
    <button id="layer-target" class="button text-alert" type="button">Layer comparison button</button>
  </main>
</body>
</html>

styles/tokens.cssでレイヤー順を最初に登録し、共有値を置きます。

@layer tokens, base, layout, components, utilities;
@layer tokens {
  :root { --ink: #183044; --accent: #166b92; --space-card: 1.25rem; }
}

styles/base.cssには文書の基本とフォーカスを置きます。IDの指定はレイヤーとの比較用です。

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; padding: 1rem; color: var(--ink); font-family: sans-serif; line-height: 1.6; }
  h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
  button { font: inherit; cursor: pointer; }
  :focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
  #layer-target { color: #183044; }
}

styles/layout.cssで領域の間隔を管理します。

@layer layout {
  .demo { display: grid; gap: 1rem; max-width: 44rem; margin-inline: auto; }
  .demo > h1 { margin-block: 0 .5rem; }
  .demo > button { justify-self: start; }
}

styles/components.cssではカードとボタンを管理します。カード内の見出し色と、ホバー・内部のフォーカス時の枠を指定します。

@layer components {
  .card {
    padding: var(--space-card); border: 2px solid #90a4b2; border-radius: .5rem;
    & h2 { margin-top: 0; color: var(--accent); }
    &:hover, &:focus-within { border-color: var(--accent); }
  }
  .button {
    padding: .65rem 1rem; color: var(--accent); background: #eaf4fa;
    border: 1px solid #166b92; border-radius: .3rem;
  }
}

styles/utilities.cssは比較用の補助クラスです。通常宣言では前のレイヤーより優先されます。

@layer utilities {
  .text-alert { color: #9d2632; }
}

ファイル・ネスト・レイヤーを分けて試す

  1. カード内の見出しと通常ボタンは青、外の見出しは基本色、比較ボタンは赤かを見ます。Tabでフォーカスも確認します。
  2. componentsの& h2だけをコメントにし、カード見出しの色を比べて戻します。
  3. HTMLで両ボタンにtext-alertを付けると内側も赤になります。内側へ追加したクラスだけを外します。
  4. components末尾の@layerの外へ.button { color: #28633b; }を追加します。両方が緑になることを見て削除します。
  5. baseのID色だけをimportantにし、比較ボタンを見ます。utilitiesもimportantにしても前のレイヤーが保たれるか確認し、両方のimportantを外します。
  6. componentsへのHTMLリンクだけを外し、形が消える差を見て再接続します。

読み込めない指定と上書きを切り分ける

まずNetworkやSourcesでファイルが読めたか、次にStylesでセレクターが一致するかを見ます。ファイルがなければ保存、名前、拡張子、相対パスを調べます。ルールが見えなければHTMLクラスと構文対応を確認します。接続は外部CSSの適用を参照してください。

打ち消しがあればレイヤー、重要度、詳細度、スコープ、順番を追います。変数なら宣言範囲も見ます。不要なimportantを重ねず、責任や衝突の原因を直し、基本・ホバー・フォーカスを再確認します。