Parts 2–3 established the framework of "invariants" and "feedback loops." Part 3 focused on feedback message design; this part defines the concrete rule system those messages enforce — the principles and structure of SpiraCSS.
Single Principle: Parents Handle Layout, Children Handle Only Internals
Every rule in SpiraCSS derives from one principle:
The parent decides the child's layout; the child only writes its own internals.
CSS layout is fundamentally a "parent arranges children" mechanism. You set display: flex on the parent and align-self on the child. Since a child's margin also affects the parent's layout, it should be controlled by the parent.
All structural rules are derived from this principle.
Block and Element — Two Structural Units
SpiraCSS classifies all classes into Blocks and Elements.
| Type | Naming Convention | Example | Role |
|---|---|---|---|
| Block | Two-word kebab-case |
.feature-card, .card-header
|
Independent component unit |
| Element | Single-word |
.title, .body, .icon
|
Constituent part within a Block |
This naming convention corresponds to BEM's "Block__Element," but with a crucial difference: the naming pattern itself expresses the structure. Two words mean Block, one word means Element — no room for interpretation. (This naming rule is the default; case conventions can be customized in spiracss.config.js.)
The allowed parent-child relationships are also explicit:
| Parent | Child | Allowed |
|---|---|---|
| Block | Block | ✅ |
| Block | Element | ✅ |
| Element | Block | ❌ |
| Element | Element | ✅ (with depth limit) |
Placing a Block under an Element is forbidden. Element-to-Element nesting is allowed but has a depth ceiling (default 4 levels, configurable via elementDepth). The fundamental structure is "Blocks own children." This doesn't restrict DOM nesting itself; it constrains only responsibility-bearing class relationships. This constraint automatically determines component boundaries.
"What if Element nesting gets too deep?" The answer is straightforward: if a unit has independent responsibility, promote it to a Block (e.g., .title → .title-box). Decorative elements are handled with tag selectors or pseudo-elements. Tag selectors are always scoped within a Block (e.g., .feature-card > .title > span), so there's no external leakage. This decision is also mechanical: if you need independent responsibility, promote to Block; otherwise, use Element or tag selectors.
Property Responsibility Separation
The "parent handles layout, child handles internals" principle directly maps to CSS property placement rules.
// Parent Block
.card-list {
display: flex; // Container property → on itself
gap: 24px; // Container property → on itself
> .feature-card {
margin-top: 16px; // Item property → specified from parent
flex: 1; // Item property → specified from parent
}
}
// Child Block (separate file)
.feature-card {
padding: 16px; // Internal property → on itself
text-align: center; // Internal property → on itself
> .title {
font-size: 18px; // Internal property → on itself
}
}
Properties fall into three categories:
-
Container properties (
display,gap,justify-content, etc.) → Write on the Block itself -
Item properties (
margin,flex,order,align-self, etc.) → Write in the parent Block's> .childselector -
Internal properties (
padding,font-size,color, etc.) → Write on the selector itself
This three-way classification reflects how CSS layout models (Flexbox, Grid) work. Even before naming or file organization, this classification is the starting point of CSS architecture.
Writing margin-top on the child Block itself triggers a lint error. The error message says: "Specify margin-top from the parent Block using the > .child-name selector." No need to memorize — just write and the lint will tell you.
This rule meets the three criteria for invariants from Part 2. Binary evaluation: Whether margin-top is in the parent's > .child or on the child itself can be unambiguously determined. Syntax-verifiable: Determined by analyzing SCSS selector structure alone; no runtime needed. Locally verifiable: Only the target SCSS file needs to be examined. That's why automated lint verification works.
1 Block = 1 File
SpiraCSS separates files by Block.
feature-card/
├── feature-card.scss ← Root Block
└── scss/
├── card-header.scss ← Child Block
├── card-body.scss ← Child Block
└── index.scss ← @use aggregation
The root Block file loads child Blocks:
@use "sass:meta";
.feature-card {
@include meta.load-css("scss");
> .card-header {
// @rel/scss/card-header.scss
margin-bottom: 16px;
}
> .card-body {
// @rel/scss/card-body.scss
}
}
This structure has three advantages:
- Physical prevention of class name collisions: You can't create two files with the same name in the same folder. This is not just a rule; the structure makes collisions impossible
- Forced responsibility separation: Child Blocks write only their own internals in their own files. There's no way to interfere with the parent's layout
-
Navigation without searching:
@relcomments link between files, enabling one-click navigation to related files
Variant and State Separation
BEM modifiers used a single mechanism to express both visual variations and dynamic states. SpiraCSS separates these into two:
| Type | Purpose | HTML | SCSS |
|---|---|---|---|
| Variant | Static variations (size, color scheme) | data-variant="primary" |
&[data-variant="primary"] |
| State | Dynamic states (open/close, selection, loading) | data-state="active" |
&[data-state="active"] |
.feature-card {
padding: 16px;
background: white;
&[data-variant="highlight"] {
background: #f0f8ff;
}
// --interaction
@at-root & {
transition: box-shadow 0.3s ease;
&[data-state="disabled"] {
opacity: 0.5;
pointer-events: none;
}
&:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
}
}
Variants go in the base structure section; States go in the --interaction section. Stylelint also enforces this placement.
SCSS Section Structure
The internal file structure is also defined. A single Block file consists of up to three sections:
- Base structure: Block layout, child placement, Variants
- --shared (optional): Styles shared among descendants within the Block
- --interaction (optional): States, hover, focus, ARIA, transitions, animations
"Where do I write hover?" "Where does transition go?" — these commonly ambiguous decisions in practice are resolved unambiguously by the section structure.
The Big Picture So Far
SpiraCSS's design can be summarized as follows:
Single principle (parent handles layout, child handles internals)
├── Naming: Block (two-word) / Element (single-word)
├── Structure: Element > Block forbidden, Element depth limit
├── Properties: Container / Item / Internal — 3 categories
├── Files: 1 Block = 1 file
├── State: Variant / State separation
└── Sections: Base / --shared / --interaction
Every rule derives from the single principle, and every rule is verifiable by Stylelint. No need to memorize conventions — just write and the lint will tell you.
The next part covers the tools and procedures for adopting this design in practice. How do you integrate it into a real project, and how do humans and AI coexist?
SpiraCSS's design specs, tools, and source code are all open source.
- SpiraCSS: https://spiracss.jp
- GitHub: https://github.com/zetsubo-dev/spiracss
Top comments (0)