spiracss/property-placement
プロパティ配置(コンテナ / アイテム / 内部)を検証します。
目的
- レイアウト責務を親子で分離し、構造の意味を明確にする
- レイアウトの決定が子側に漏れることを防ぐ
検証内容
- page-root セレクタ(単独の
bodyまたは単独の#id)ではレイアウト系プロパティを禁止
| カテゴリ | root .block | > .child-block | 補足 |
|---|---|---|---|
| コンテナ | ✅ | ❌ | 子 Element(> .element)は root 扱い — ✅ |
| アイテム | ❌ | ✅ | 直下の子全般(> .child Block / Element とも)。+ / ~ は末尾の兄弟指定としてのみ許可 |
| 内部 | ✅ | ❌ | 子 Element は root 扱い — ✅。padding / overflow / width·height·inline-size·block-size·min·max(sizeInternal 有効時)を含む |
- 縦方向マージンの片側統一(
marginSide) positionの制限(position: true)@extendは常にエラー、@at-rootは interaction セクション専用
✅ 適切
.parent-block { display: flex; // コンテナ gap: 16px;
> .child-block { margin-top: 16px; // アイテム }
> .title { padding: 8px; // 内部 }}❌ 不適切
.parent-block { > .child-block { padding: 16px; // ❌: 子 Block の内部プロパティ }}body { display: flex; // ❌: page-root でレイアウト系は不可}.parent-block { > .child-block { margin-bottom: 16px; // ❌: marginSide が top の場合 }}理由
- 親がコンテナレイアウトを決め、親直下で子を調整し、子は内部だけを書くという責務分離を保つため
- 縦方向マージンを片側に統一すると余白計算が単純になり、重複を防げる
@extend/ 不適切な@at-rootは配置追跡ができず、構造が崩れる
sizeInternal について(なぜ size を internal 扱いにするか)
size 系(width / height / inline-size / block-size / min-* / max-*)は内部(コンポーネントの既定)としても外部(配置先の制約)としても使われやすく、プロパティ名でも値でも意図は一意に決まりません。
| 例 | ありがちな意図 | でも… |
|---|---|---|
width: 1em | 内部(アイコン) | font-size 依存で文脈が絡む |
width: 100% | アイテムプロパティ(親に合わせる) | コンポーネントの既定としても使われる |
max-width: 600px | 内部(上限の契約) | レイアウト都合で上書きしたくなることもある |
max-width: 50% | アイテムプロパティ(親に対する割合) | 意味づけがレイアウト設計に依存する |
width: fit-content | 内部(内容に合わせる) | 配置先でサイズを変えたいケースもある |
width: min(320px, 100%) | 内部(上限付き可変) | 100% が親依存で判断が割れやすい |
SpiraCSS はこの曖昧さを解消するため、デフォルト(sizeInternal: true)では size 系も内部プロパティに統一しています。ルールをシンプルに保ち、意図の推測なしに配置を検証できるようにするためです。
CSS 変数を使わず > .child-block に直接 size を書きたい場合は sizeInternal: false も選べます。この場合、size 系プロパティはアイテムに再分類されるのではなく、配置検証の対象外(unchecked)になります。どこに書いてもルール違反にはなりませんが、その分レビュー依存が増えます。
position について(子 Block の position 制限)
position: true(デフォルト)の場合、子 Block セレクタに対して追加の制限が入ります。
| 値 | > .child-block での可否 | 条件 |
|---|---|---|
static | ✅ | — |
relative | ⚠️ 条件付き | offset(top / right / bottom / left / inset*)が最低 1 つ必要。なければ子 Block 自身のファイルで指定する |
absolute | ⚠️ 条件付き | offset が最低 1 つ必要。なければ子 Block 自身のファイルで指定する |
fixed | ❌ | 子 Block 自身のファイル側で指定する |
sticky | ❌ | 子 Block 自身のファイル側で指定する |
position の値が動的(var(...) 等) | ❌ | offset 判定が不可 |
initial / inherit / unset / revert / revert-layer | — スキップ | 検証対象外(CSS-wide keywords + initial は意図的なリセットとみなす) |
offset は同一の「ラッパー文脈」内にある必要があります。
@media/@supports/@container/@layerは透過扱い@scopeは文脈境界になる(内側に入ると別文脈)responsiveMixinsに含まれる@includeは透過扱い
例外 / 注意
min-*の値が0の場合は子 Block でも許可(sizeInternalが有効な場合)- 禁止側の値が
0/auto/initialは許可 - 値全体が
inherit/unset/revert/revert-layer単体ならスキップ - 禁止側が
var(...)/ 関数値 /$.../#{...}の場合はエラー @scopeは文脈境界、responsiveMixinsに含まれる@includeは透過扱い- CSS Modules の
:global/:localは透過して解析する(中身のセレクタに対してチェックする) u-は固定で外部扱い(externalに追加指定したクラスも対象外)- セレクタの検証範囲: このルールは直下子結合子チェーン(
>)として解決できるセレクタを配置チェック対象にします。子孫結合子(スペース)や途中の+/~など未対応のパターンは配置チェック対象外として静かにスキップされます。marginSideTagsがtrue(デフォルト)の場合、セレクタチェーン上のタグセレクタにはmarginSideViolationチェックを適用します(:is(main)のように疑似クラス引数内にだけあるタグはタグセレクタ扱いしません)。解決されたセレクタ組み合わせが内部上限(現在 1,000)を超えた場合も、残りはスキップされます(selectorResolutionSkipped警告が出ます)。
エラー一覧
containerInChildBlock(子 Block にコンテナプロパティ)
例:
// ❌.parent-block { > .child-block { display: flex; gap: 8px; }}
// ✅.parent-block { display: flex; gap: 8px;}理由: コンテナレイアウトは親または子自身が持つ
itemInRoot(ルート Block にアイテムプロパティ)
例:
// ❌.child-block { margin-top: 16px;}
// ✅.parent-block { > .child-block { margin-top: 16px; }}理由: ルート Block は自分の配置を決めず、親が決める
selectorKindMismatch(セレクタ種別の混在)
例:
// ❌.parent-block { > .title, > .child-block { margin-top: 16px; }}
// ✅.parent-block { > .title { margin-top: 16px; }
> .child-block { margin-top: 16px; }}理由: ルート / Element / child Block を混ぜると配置判定ができない
marginSideViolation(縦方向マージンの禁止側使用)
例:
// ❌.parent-block { > .title { margin-bottom: 16px; }}
// ✅.parent-block { > .title { margin-top: 16px; }}理由: 片側統一で余白計算を単純化する
internalInChildBlock(子 Block に内部プロパティ)
例:
// ❌.parent-block { > .child-block { padding: 16px; }}
// ✅.child-block { padding: 16px;}理由: 内部プロパティは子自身が持つ
positionInChildBlock(子 Block の position 制限)
例:
// ❌.parent-block { > .child-block { position: fixed; top: 0; }}
// ❌.parent-block { > .child-block { position: relative; }}
// ❌.parent-block { > .child-block { position: var(--pos); top: 0; }}
// ✅.parent-block { > .child-block { position: relative; top: 0; }}理由: 親子のレイアウト文脈を保ち、offset 判定を可能にする
pageRootContainer(page-root にコンテナプロパティ)
例:
// ❌body { display: flex; gap: 12px;}
// ✅.page-root { display: flex; gap: 12px;}理由: page-root は装飾専用で、レイアウトは Block に委譲する
pageRootItem(page-root にアイテムプロパティ)
例:
// ❌body { margin-top: 16px;}
// ✅.page-root { > .child-block { margin-top: 16px; }}理由: page-root 自身の配置は持たない
pageRootInternal(page-root に内部プロパティ)
例:
// ❌body { padding: 16px;}
// ✅.page-root { padding: 16px;}理由: 内部プロパティは Block 側で持つ
pageRootNoChildren(page-root の複合セレクタ)
例:
// ❌body > .main { color: #222;}
// ✅body { color: #222;}理由: page-root は単独セレクタとして扱う
forbiddenAtRoot(interaction 以外での @at-root)
例:
// ❌.parent-block { @at-root & { color: red; }}
// ✅.parent-block { // --interaction @at-root & { color: red; }}理由: @at-root は構造を壊すため interaction 専用
例外: 外部クラスのみのルート。ルートのセレクタが external.classes / external.prefixes に一致する
外部クラスだけで構成され、// --interaction コメントがある場合は @at-root を許可します。
タグ / ID / 属性 / 擬似クラスや、外部ではないクラスが混ざると無効です。
例(外部クラスのみのルート):
// ✅(外部ルート + interaction).swiper-pagination { // --interaction @at-root & { &:focus-visible { outline: 3px solid #000; } }}forbiddenExtend(@extend の禁止)
例:
// ❌.parent-block { @extend %placeholder;}
// ✅.parent-block { @include placeholder();}理由: @extend は暗黙依存を生み配置追跡ができない
selectorResolutionSkipped(セレクタ解決のスキップ)
内容:
.parent-block { &.-a, &.-b, &.-c, &.-d, &.-e, &.-f, &.-g, &.-h { > .child-block { padding: 1px; } }}理由: 解析が爆発するほど複雑なセレクタはスキップされる
selectorParseFailed(セレクタ解析失敗)
例:
// ❌.parent-block { > : { padding: 4px; }}
// ✅.parent-block { > .title { padding: 4px; }}理由: 解析できないセレクタは検証できない