コンテンツにスキップ

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 に追加指定したクラスも対象外)
  • セレクタの検証範囲: このルールは直下子結合子チェーン(>)として解決できるセレクタを配置チェック対象にします。子孫結合子(スペース)や途中の + / ~ など未対応のパターンは配置チェック対象外として静かにスキップされます。marginSideTagstrue(デフォルト)の場合、セレクタチェーン上のタグセレクタには 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;
}
}

理由: 解析できないセレクタは検証できない

設定