コンテンツにスキップ

spiracss/class-structure

SpiraCSS の命名規則とセレクタ構造を検証します。

目的

  • Block / Element の構造を機械的に判定できる状態に保つ
  • 親子構造の揺れを抑え、責務の分離を安定させる

検証内容

  • 命名(Block / Element / Modifier)
  • Block / Element の親子関係(Element > Block などを禁止)
  • Block 直下の > を強制(shared は緩和、interaction は構造検査の対象外)
  • Block の子セレクタは Block 内にネストして書く(トップレベルの .block > .child は不可)
  • shared / interaction セクションの配置位置
  • 1 ファイル 1 ルート Block(rootSingle
  • ルート Block とファイル名の一致(rootFile
  • selectorPolicy に応じた data/state の扱い

OK

.sample-block {
> .title {
font-size: 16px;
}
// --shared
.helper {
color: #999;
}
// --interaction
@at-root & {
&:hover {
opacity: 0.8;
}
}
}

NG

.sample-block {
.title { // NG: 基本構造では直下セレクタに `>` を付ける
font-size: 16px;
}
}
.sample-block {
> .title {
> .detail {
> .label {
> .meta {
> .text { // NG: Element 連鎖が深すぎる(elementDepth 超過)
color: #333;
}
}
}
}
}
}

理由

  • > を強制することで、親が子の並びを決める構造が明確になる
  • Element 連鎖は深さを制限し、責務の境界がブレにくくなる

例外 / 注意

  • shared セクションでは > の強制のみ緩和される
  • interaction セクションでは構造検証を行わない(命名検証は継続)
  • rootFile*.module.scss を許可します(CSS Modules。.module は無視されます)
  • rootFile の検証で childDir 配下のファイル名は childFileCase を使います(設定オプション参照)

エラー一覧

invalidName(命名規則違反)

例:

// NG
.HeroBanner {
color: #222;
}
.titleText {
font-weight: 600;
}
// OK
.hero-banner {
color: #222;
}
.title {
font-weight: 600;
}

理由: 命名で構造を判定するため

補足: stylelint.base.naming / naming.customPatterns の設定に従う

elementChainTooDeep(Element 連鎖が深すぎる)

例:

// NG
.card-list {
> .item {
> .content {
> .title {
font-size: 16px;
}
}
}
}
// OK
.card-list {
> .item {
> .title {
font-size: 16px;
}
}
}

理由: Element 連鎖が深いと責務が曖昧になりやすい

補足: 深さ上限は elementDepth

elementCannotOwnBlock(Element の下に Block がある)

例:

// NG
.card-list {
> .item {
> .price-tag {
color: #333;
}
}
}
// OK
.card-list {
> .price-tag {
color: #333;
}
}

理由: Element は Block の部品であり親にならない

blockDescendantSelector(Block 直下の孫セレクタ)

例:

// NG
.card-list {
> .item > .title {
margin-top: 8px;
}
}
// OK
.card-list {
> .item {
margin-top: 8px;
}
}

理由: 親が子の並びを決める構造を明確にする

blockTargetsGrandchildElement(孫 Element の直接指定)

例:

// NG
.card-list {
> .price-tag {
> .amount {
font-weight: 700;
}
}
}
// OK
.price-tag {
> .amount {
font-weight: 700;
}
}

理由: 親 Block が孫 Element を直接触らない

tooDeepBlockNesting(Block の多段ネスト)

例:

// NG
.card-list {
> .price-tag {
> .icon-badge {
color: #fff;
}
}
}
// OK
.card-list {
> .price-tag {
color: #fff;
}
}

理由: Block > Block > Block の連鎖は責務が不明瞭になる

multipleRootBlocks(ルート Block が複数)

例:

// NG
.hero-banner {
color: #222;
}
.card-list {
color: #333;
}
// OK
.hero-banner {
color: #222;
}

理由: ファイルの入口を一つに揃える

needChild(> 直下指定がない)

例:

// NG
.card-list {
.title {
margin-top: 8px;
}
}
// OK
.card-list {
> .title {
margin-top: 8px;
}
}

理由: 直下関係を明示して責務を固定する

needChildNesting(トップレベルの子セレクタ禁止)

例:

// NG
.hero-banner > .title {
font-size: 16px;
}
// OK
.hero-banner {
> .title {
font-size: 16px;
}
}

理由: Block 内に構造を集約し、読み順を統一する

sharedNeedRootBlock(shared コメントの位置)

例:

// NG
.card-list {
> .title {
// --shared
.helper {
color: #999;
}
}
}
// OK
.card-list {
// --shared
.helper {
color: #999;
}
}

理由: shared セクションの範囲を一意にする

needAmpForMod(Modifier は &.

例:

// NG
.card-list.-primary {
color: #111;
}
// OK
.card-list {
&.-primary {
color: #111;
}
}

理由: Modifier は Block 内の状態として扱う

needModifierPrefix(& への付与が Modifier ではない)

例:

// NG
.card-list {
&.title {
color: #111;
}
}
// OK
.card-list {
&.-primary {
color: #111;
}
}

理由: & に付与できるのは Modifier のみ

disallowedModifier(data モード時の Modifier 禁止)

例:

// NG
.card-list {
&.-primary {
color: #111;
}
}
// OK
.card-list {
&[data-variant="primary"] {
color: #111;
}
}

理由: Variant/State は data 属性で表現する設計

invalidVariantAttribute(variant が class モード)

例:

// NG
.card-list {
&[data-variant="primary"] {
color: #111;
}
}
// OK
.card-list {
&.-primary {
color: #111;
}
}

理由: class モードでは Modifier を使う

invalidStateAttribute(state が class モード)

例:

// NG
.card-list {
&[data-state="active"] {
opacity: 1;
}
}
// OK
.card-list {
&.-active {
opacity: 1;
}
}

理由: class モードでは Modifier を使う

invalidDataValue(data 値の命名違反)

例:

// NG
.card-list {
&[data-variant="primary-dark-large"] {
color: #111;
}
}
// OK
.card-list {
&[data-variant="primary-dark"] {
color: #111;
}
}

理由: data 値も命名ルールに従わせる

rootSelectorMissingBlock(ルート Block を含まないセレクタ)

例:

// NG
.swiper {
margin-top: 8px;
}
// OK
.tab-panels {
> .swiper {
margin-top: 8px;
}
}

理由: ルート Block を起点に構造を読むため

missingRootBlock(ルート Block が無い)

例:

// NG
.title {
font-size: 16px;
}
// OK
.card-list {
font-size: 16px;
}

理由: 1 ファイル 1 ルート Block が前提

selectorParseFailed(セレクタ解析失敗)

例:

// NG
.card-list {
> : {
color: #111;
}
}
// OK
.card-list {
> .item {
color: #111;
}
}

理由: 解析できないセレクタは検証をスキップせざるを得ない

fileNameMismatch(ファイル名と Block 名の不一致)

例:

list.scss
// NG
.card-list {
color: #111;
}
// OK
// file: card-list.scss
// (CSS Modules: card-list.module.scss も許可)
.card-list {
color: #111;
}

理由: ファイルと Block の対応を固定する

設定