CSS architecture (BEM, etc.)

Have Agents add styles under BEM, ITCSS, CSS Modules, or utility-first setups with clear cascade order and global entry boundaries; this page ties naming, flow, and a small lab together.

The SKILL must state block/element/modifier rules (or project equivalents), forbidden selector shapes (e.g. deep nesting, global tag selectors), and @layer or import order when used.

With JS frameworks, document CSS Modules :global thresholds, styled-components theme injection, and avoid mixing paradigms for the same look, which invites specificity wars.

For third-party overrides: prefer public APIs, variables, or wrapper classes, and record when !important is allowed by policy.

  • Stylelint rules aligned with naming, nesting depth, and custom property conventions.
  • Critical CSS or build splits: Agents should not dump large rules into the wrong entry.
  • Design tokens: cross-reference the design-system skill for color and spacing references.

Style extension flow (skill-flow-block)

  [ Read SKILL: naming, forbidden selectors, entry files ]
        │
        ▼
  ┌─────────────┐     Choose BEM or equivalent (SUIT, rscss)
  │ Scope & names │── no bare tags, deep descendants; one root block per component
  └─────────────┘
        │
        ▼
  ┌─────────────┐     @layer order / ITCSS tiers / import graph
  │ Right layer   │── new rules do not jump above utilities unless SKILL allows
  └─────────────┘
        │
        ▼
  ┌─────────────┐     Modules: local by default; :global needs rationale
  │ Framework chk │── third party: API → variables → wrapper → hard override last
  └─────────────┘
        │
        ▼
  ┌─────────────┐     Stylelint + review checklist; exceptions in SKILL
  │ Validate/docs │── !important and deep selectors must be traceable
  └─────────────┘

Lock naming and layer first, then write rules; if the team uses @layer, new styles must use the layer names from the SKILL instead of “rescuing” with higher specificity later.

BEM: block, element, modifier

/* BEM naming: block__element--modifier */
.card { padding: var(--space-4); }               /* Block */
.card__title { font-size: var(--font-size-lg); } /* Element: expresses role, not tag name */
.card__body  { color: var(--color-text-muted); }
.card--compact { padding: var(--space-2); }      /* Modifier: variant of block */
.card--featured .card__title { color: var(--color-brand); }

/* Multiple modifiers: stack classes, not chained */
<div class="card card--compact card--dark">   <!-- ✅ correct -->
<div class="card card--compact--dark">        <!-- ❌ anti-pattern: chained modifier -->

/* ❌ anti-pattern: structural coupling (high specificity + fragile) */
/* .card .title { } — not BEM, relies on DOM hierarchy */

/* Mixing with utility classes (team must agree) */
<div class="card mt-4 mb-2">  <!-- mt/mb from utilities; card is the BEM block -->
  • Element names should express responsibility (__title) not DOM tag names (__h2).
  • With multiple modifiers, stack classes side by side (.card .card--compact .card--dark) instead of chaining (--compact--dark).

@layer cascade layer usage examples

/* @layer: declare layer order — later layers have higher priority */
@layer reset, tokens, base, components, utilities;

/* reset layer: lowest priority */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}

/* tokens layer: CSS variables — no specificity issues */
@layer tokens {
  :root { --color-brand: #3b82f6; --space-4: 1rem; }
}

/* components layer: BEM component styles */
@layer components {
  .card { padding: var(--space-4); border-radius: 0.5rem; }
  .card--elevated { box-shadow: var(--shadow-md); }
}

/* utilities layer: highest priority — single class overrides */
@layer utilities {
  .mt-4  { margin-top: var(--space-4) !important; }  /* utility may use !important */
  .sr-only { position: absolute; clip: rect(0,0,0,0); }
}

/* Unlayered styles (no @layer) have higher priority than all layers */
.third-party-override { color: var(--color-brand); }
  • Before editing, check the SKILL layer list: which @layer a new file belongs in, or which partial it follows.
  • Specificity strategy: prefer single-class selectors; to raise weight, adjust layer order instead of adding deeply nested descendants.

CSS Modules vs CSS-in-JS

/* CSS Modules: local by default; build generates unique class names */
/* Card.module.css */
.card { padding: var(--space-4); }       /* → .Card_card__xyz unique class */
.title { font-size: var(--font-size-lg); }

/* Global leakage: must document the reason */
:global(.third-party-class) { color: red; } /* reason: third-party hook class */

// Card.tsx (React)
import styles from './Card.module.css';
const Card = () => <div className={styles.card}><h2 className={styles.title}>...</h2></div>;

// CSS-in-JS (styled-components): inject theme tokens from a single source
import styled, { ThemeProvider } from 'styled-components';

const StyledCard = styled.div`
  padding: ${({ theme }) => theme.space[4]};
  border-radius: ${({ theme }) => theme.radii.md};
`;

// CSS Modules: zero runtime overhead, build-time extraction — preferred for SSR
// CSS-in-JS: dynamic theme/props-driven styles, runtime cost — suited for design systems
  • CSS-in-JS: inject theme tokens from a single source to avoid drift between runtime and static CSS variables.
  • Overriding libraries: check classNames / slots / CSS variables first; a BEM wrapper block beats editing internal library classes across upgrades.

Stylelint, tokens, and critical CSS

  • Stylelint: max-nesting-depth, selector-max-id, custom property naming, and BEM regex should match the SKILL.
  • Critical CSS: if the build inlines critical path rules, do not put footer-only rules in the critical entry.
  • Design tokens: spacing and color via var(--space-*) etc.; no magic numbers unless the SKILL allows an exception directory.

BEM class builder lab

Build a single BEM class string (one class, no spaces) for pasting into SKILL examples or templates; block is required, element and modifier optional.

Rules: .{block}, .{block}__{element}; modifier suffix --{modifier} on block or element. Input is lowercased; invalid characters become hyphens.

---
name: css-architecture
description: Extend styles under BEM/ITCSS etc. and control cascade
---
# Naming conventions
1. BEM: .block, .block__element, .block--modifier; element names express responsibility, not tag names
2. Forbidden: bare tag global selectors, more than 3 levels of descendant nesting, chained modifiers (--a--b)
3. Multiple modifiers: stack classes side by side (.card .card--compact .card--dark), never chain

# Cascade control
4. @layer declaration order: reset → tokens → base → components → utilities
5. New styles must land in the layer name specified in the SKILL — do not skip layers
6. Utility layer may use !important (the only allowed exception within a layer)
7. Unlayered styles have higher priority than all layers; exploit this for third-party overrides

# Framework and scoping
8. CSS Modules: local by default; :global requires a comment with the reason (e.g. third-party hook class)
9. CSS-in-JS: inject theme tokens from ThemeProvider single source; do not hardcode values
10. CSS Modules vs CSS-in-JS: prefer CSS Modules (zero runtime) for SSR; use CSS-in-JS when dynamic props-driven styles are required

# Third-party overrides
11. First choice: component classNames / slot / CSS variable API
12. Second choice: add override styles to a BEM wrapper block
13. Last resort: override internal class names directly (document with PR link and upgrade risk)
14. !important must have a traceable reason; add to the SKILL exemption list

# Quality
15. Stylelint rules: max-nesting-depth ≤ 3, selector-max-id ≤ 0, custom property naming regex

Back to skills More skills