本文へスキップ
ナビゲーションメニュー 書籍を読む

構造

責任の層によるアーキテクチャ

各層は @layer の宣言順に並ぶ。宣言順がそのまま優先順位になり、後に宣言した層ほど強い。

層が違えば、答える問いも違う。ある値をどこで一元管理するかを決めるのは Token 層の責任であり、その値を使ってページの骨格を組むのは Layout 層の責任だ。責任が重ならないから、どこに書くべきかを機械的に判定できる。

  • Token

    責任範囲

    「その値は、どこで一元管理するか」に答える層。design token の集約定義を担う。

    配置基準

    Token Test(この値はデザイントークン〔色の値、フォント名、余白、z-index 等〕か? または計算ヘルパーか?)で判定する。色・spacing・typography・motion の単一の定義源として扱う。

    --color-main, --space-md

  • Reset

    責任範囲

    「ブラウザ間の差を、どこまで無くすか」に答える層。リセットまたはノーマライズで、ブラウザ間の差異を吸収する。

    配置基準

    Reset Test(このスタイルはブラウザデフォルトの初期化か?)で判定する。

    margin: unset 等の、ブラウザ既定値の解除。

  • Foundation

    責任範囲

    「要素の初期スタイルは、何を基準にするか」に答える層。body のフォントや見出し・リンク色など、要素そのものへの基本スタイルを担う。

    配置基準

    Foundation Test(このスタイルは、全ページ共通の基本スタイルか?)で判定する。

    :where(body) のフォントと行間、見出し・リンクの基本スタイル。

  • Layout

    責任範囲

    「ページの骨格は、どう構成するか」に答える層。header・footer・main 等の構造を組む。

    配置基準

    Layout Test(このスタイルは、配置・寸法・空間の確保か? 中身の見た目は変わらないか?)で判定する。

    .l-header, .l-footer, .l-section

  • Component

    責任範囲

    「この部品は、別のサイトへそのまま持ち出せるか」に答える層。再利用可能な小単位の UI を担う。

    配置基準

    Portability Test(そのパーツを別のサイトにそのまま持っていけるか?)で判定する。

    .c-button, .c-card, .c-badge

  • Project

    責任範囲

    「このパーツは、置く場所で決まるか」に答える層。プロジェクト固有の組み合わせを担う。

    配置基準

    Responsibility Test(このスタイルは、パーツ自身の視覚的責任か?)で、Portability Test だけでは判断できない場合を補う。

    .p-home-hero, .p-home-features

  • Animation

    責任範囲

    「動きは、どこに集約するか」に答える層。装飾的なアニメーションを分離して集約する。

    配置基準

    Animation Test(その動きを無効化しても、インタラクションの意味が伝わるか?)で判定する。装飾的な動き(fade-in 等)はこの層に分離し、hover の色変化のような機能的トランジションは、その Block が属する層に残す。prefers-reduced-motion への対応はここで一元管理する。

    @keyframes fade-in, prefers-reduced-motion 対応。

  • Utility

    責任範囲

    「単一の上書きは、どこで最終保証するか」に答える層。単一目的の上書きを担う。

    配置基準

    Utility Test(このスタイルは特定の Block に帰属せず、単一プロパティ〔または密接に関連する最小プロパティセット〕で完結するか?)で判定する。各プロパティに !important を付与し、@layer の外にある unlayered CSS に対しても最終上書きを構造的に保証する(spec §5.8 MUST)。

    .u-visually-hidden, .u-hidden-sp

判断テスト

「どの層に書くべきか」を、センスではなく問いで決める。

  • Portability Test

    そのパーツを別のサイトにそのまま持っていけるか? Yes なら Component。

  • Responsibility Test

    このスタイルは、パーツ自身の視覚的責任か? Portability Test だけで判断が曖昧なときに補う。パーツ自身の責任なら Component、置く場所によって変わるなら Project。

  • Layout Test

    このスタイルは、配置・寸法・空間の確保か? 中身の見た目は変わらないか? Yes なら Layout。

判断の流れ

このスタイル、どの層に書くべきか。判断テストを順に辿ると、答えは一本の道筋として決まる。迷ったときは、各層の判断テストを上から順に当てていけばいい。

詳細度バトル可視化

ここでは 2 つの角度から @layer の効き方を見る。ひとつは層の秩序が破られる一線と、それでも最後の例外を通す仕組み。もうひとつは、Component と Project の境界をどう判定するか。

理想は、すべてのスタイルが層に入っていること。けれど現実のサイトには、層から漏れたスタイルが紛れ込む。

Before — 層の外の一行が、すべてを押しのける

Before
@layer component {
  .c-notice-card__note {
    color: var(--color-text-light);
  }
}

/* あとから誰かが足した一行、層に入れ忘れた借り物の CSS、
   ウィジェットが注入するスタイル…… */
.c-notice-card__note {
  color: red;
}

/* 層に入っていない素のスタイルは、層に入った
   すべての通常スタイルに勝ってしまう */

After — !important が最終上書きを保証する

After
@layer utility {
  .u-hidden-sp {
    @media (width < 48rem) {
      display: none !important;
    }
  }
}

!important を付けた宣言は、層の内外を問わず、通常のスタイルすべてより一段上で勝負する。だから Utility に !important を付けておけば、層から漏れたスタイルがどこかに紛れていても、最後の例外は確実に通る(spec §5.8 MUST)。「勝つためにセレクタを強くする」という動機そのものが要らなくなる。

判断の核は Portability Test(そのパーツを別のサイトにそのまま持っていけるか?)。Yes なら Component、No なら Project。曖昧な場合のみ Responsibility Test(このスタイルは、パーツ自身の視覚的責任か?)で補う。

Component(.c-button)— 別のサイトに持っていける

Component
/* 公開 API:
     --button-color             (default: var(--color-surface))
     --button-bg-color          (default: var(--color-accent))
     --button-border-color      (default: var(--color-accent))
     --button-hover-bg-color    (default: oklch(from var(--color-accent) calc(l * 1.08) c h)) */
@layer component {
  .c-button {
    --_color: var(--button-color, var(--color-surface));
    --_bg-color: var(--button-bg-color, var(--color-accent));
    --_border-color: var(--button-border-color, var(--color-accent));
    --_hover-bg-color: var(--button-hover-bg-color, oklch(from var(--color-accent) calc(l * 1.08) c h));

    display: inline-flex;
    gap: var(--space-sm);
    align-items: center;
    justify-content: center;
    padding: var(--space-sm) var(--space-lg);
    color: var(--_color);
    background-color: var(--_bg-color);
    border: var(--border-width) solid var(--_border-color);
    border-radius: var(--radius-lg);

    @media (any-hover: hover) {
      &:hover {
        background-color: var(--_hover-bg-color);
      }
    }

    /* Modifier: 静的なバリエーション */
    &.-ghost {
      --_color: var(--button-color, var(--color-main));
      --_bg-color: var(--button-bg-color, transparent);
      --_border-color: var(--button-border-color, var(--color-main));
    }
  }
}

ボタンの背景色・余白・角丸・枠線は「ボタン自身の視覚的責任」(Responsibility Test = Yes)。どのサイトにもそのまま持っていける(Portability Test = Yes)→ Component。

Project(.p-home-hero)— このサイト固有の組み合わせ

Project
@layer project {
  .p-home-hero {
    display: grid;
    place-items: center center;
    min-block-size: 60dvb;
    /* メインカラーの透明版を放射状に。Theme 変更に自動追従(相対カラー構文)*/
    background: radial-gradient(ellipse at 50% 0%, oklch(from var(--color-main) l c h / 6%) 0%, transparent 70%);
  }

  /* Hero グラデーション上のダークモード可読性調整(c-button 公開 API を上書き)*/
  .p-home-hero__sub-cta {
    --button-color: light-dark(var(--color-main), var(--color-text));
    --button-border-color: light-dark(var(--color-main), var(--color-text));
  }
}

ヒーローのグリッド配置・最小高さ・背景グラデーションは「このサイト固有のデザイン要件」で、別のサイトにそのまま持っていけない(Portability Test = No)→ Project。

境界判定の要点: .p-home-hero__sub-cta は Project 層から Component の公開 API(--button-color / --button-border-color)を上書きしている。Component(.c-button)の中身は書き換えず、公開 API 経由で文脈に合わせる。これが「Component は配置先に依存しない自己完結したパーツ、Project はそれを文脈に合わせて組み合わせる」という責任分離の実コードだ。

Component が Project のクラスや Custom Property を参照してはならない(spec §3 MUST NOT)。上書きは常に上位層(Project)から下位層(Component)の公開 API に対して行う。