CSS Architecture

Chapter 11 — CSS Architecture

Writing CSS that works is easy. Writing CSS that a team can maintain six months later is hard. Architecture is the set of decisions that keep a growing stylesheet predictable: how classes are named, how files are split, how specificity is kept low, and how the cascade is made explicit rather than accidental. This chapter covers the major methodologies, when to use each, and how modern CSS features (@layer, custom properties, nesting) change the architecture calculus.

1. The Core Problem CSS Architecture Solves

CSS has three properties that cause trouble at scale:

  • Global by default. Every selector can affect any element anywhere in the document. There is no built-in encapsulation boundary.
  • Cascade order matters. A rule declared later wins a specificity tie. Split files across multiple stylesheets or developers and source order becomes unpredictable.
  • Specificity wars. Once a component uses .card .title, anything trying to override it must match or beat that specificity — leading to ever-escalating selectors and eventual !important.

Every CSS methodology is essentially an answer to one question: how do we prevent styles from bleeding into things they weren't meant to affect? The solutions differ in where they put the constraint — in naming conventions, in tooling, in selector rules, or in the cascade itself.

/* The classic specificity war — how it escalates */ /* Developer A writes: */ .sidebar .card .title { color: #58a6ff; } /* specificity: 0-3-0 */ /* Developer B tries to override: */ .title { color: #c9d1d9; } /* 0-1-0 — loses */ .card .title { color: #c9d1d9; } /* 0-2-0 — loses */ .sidebar .card .title.override { color: #c9d1d9 !important; } /* gives up */ /* Root cause: specificity was never kept low in the first place */ /* Architecture fix: write .title { } everywhere and use the cascade to order */

2. The Major Methodologies

BEM
Block__Element--Modifier — naming convention
Strengths
  • Forces flat, low-specificity selectors
  • Class name communicates component structure
  • No tooling needed — pure naming discipline
  • Universally understood across teams
Weaknesses
  • Long, verbose class names in HTML
  • No cascade control — order still matters
  • Modifier explosion for state variants
  • Doesn't solve file organisation
Best for
  • Multi-developer teams who share templates
  • Projects without a build step
Utility-First (Tailwind)
Single-purpose classes composed in HTML
Strengths
  • Virtually no specificity conflicts
  • Design constraints enforced by available classes
  • Co-location — styles live with the element
  • PurgeCSS removes unused classes automatically
Weaknesses
  • HTML becomes verbose and hard to scan
  • Requires build tooling and config
  • Repeating identical class lists across similar elements
  • Difficult to express truly custom one-off styles
Best for
  • React/Vue/Svelte component-based projects
  • Teams that want design constraints enforced by the tool
CUBE CSS
Composition, Utility, Block, Exception
Strengths
  • Works with the cascade rather than fighting it
  • Custom properties as the primary API
  • Minimal class names — leans on HTML semantics
  • Pairs naturally with @layer and modern CSS
Weaknesses
  • Less prescriptive — requires more design decisions
  • Newer; fewer examples and ecosystem resources
  • Requires team to understand the cascade deeply
Best for
  • Projects using a design token system
  • Content-heavy sites with variable markup
CSS Modules / Scoped CSS
Tooling-enforced local scope
Strengths
  • True encapsulation — no naming discipline required
  • Collisions are physically impossible
  • Works with any class naming style
  • Dead code elimination via tree-shaking
Weaknesses
  • Requires bundler integration (webpack, Vite)
  • Global styles (resets, themes) need :global() escape hatch
  • Hard to share styles across module boundaries
  • Generated class names make DevTools harder to use
Best for
  • Large React/Next.js or Vue/Nuxt projects
  • Teams where naming discipline is hard to enforce

3. BEM in Depth

/* BEM: Block__Element--Modifier */ /* Block — standalone component */ .card { background: var(--surface); border-radius: 8px; padding: 1.5rem; } /* Elements — parts of the block, separated by __ */ .card__header { margin-bottom: 1rem; } .card__title { font-size: 1.25rem; font-weight: 700; } .card__body { color: var(--text-muted); } .card__footer { margin-top: 1rem; display: flex; gap: 0.5rem; } /* Modifiers — variants, separated by -- */ .card--featured { border: 2px solid var(--color-accent); } .card--compact { padding: 0.75rem; } .card--loading { opacity: 0.6; pointer-events: none; } /* HTML — block and modifier on the same element */ /* <article class="card card--featured"> */ /* <header class="card__header"> */ /* <h2 class="card__title">...</h2> */ /* </header> */ /* <div class="card__body">...</div> */ /* </article> */ /* BEM rules: */ /* 1. Elements are always children of their Block — never nest .card__title inside another block */ /* 2. Never use element selectors like .card h2 — always .card__title */ /* 3. Modifiers never stand alone — always paired with the block: class="card card--featured" */ /* 4. Specificity stays flat: all selectors are one class = 0-1-0 */ /* BEM + nesting (modern hybrid) */ .card { background: var(--surface); &__title { font-size: 1.25rem; } /* compiles to .card__title */ &__body { color: var(--text-muted); } &--featured { border: 2px solid var(--color-accent); } } /* Co-location of BEM rules without sacrificing flat specificity */

4. CUBE CSS

CUBE CSS (Composition, Utility, Block, Exception) was coined by Andy Bell. Its central premise is that the cascade is not the enemy — fighting it with ever-tighter selectors is. Instead, use the cascade deliberately: global composition styles flow from broad selectors, utilities make targeted overrides, blocks handle components, and exceptions handle the rare one-off.

/* C — Composition: layout primitives, flow, cluster, stack */ /* These use element and low-specificity selectors intentionally */ .flow > * + * { margin-block-start: var(--flow-space, 1em); } .cluster { display: flex; flex-wrap: wrap; gap: var(--cluster-gap, 1rem); align-items: var(--cluster-align, center); } .stack { display: flex; flex-direction: column; gap: var(--stack-gap, 1.5rem); } .sidebar-layout { display: flex; flex-wrap: wrap; gap: var(--sidebar-gap, 1rem); > :first-child { flex-basis: var(--sidebar-w, 20rem); } > :last-child { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; } } /* U — Utilities: single-purpose, very low specificity */ .text-center { text-align: center; } .text-muted { color: var(--color-text-muted); } .visually-hidden { position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0); overflow: hidden; } /* B — Block: component-level styles, higher specificity acceptable */ .card { background: var(--card-bg, var(--color-surface)); border-radius: var(--card-radius, 8px); padding: var(--card-padding, 1.5rem); } /* E — Exception: data attributes for contextual overrides */ .card[data-variant="featured"] { --card-bg: var(--color-featured-surface); } /* HTML usage: composition + utility + block + exception */ /* <ul class="cluster" role="list"> */ /* <li class="card" data-variant="featured"> */ /* <div class="stack"> */ /* <h2 class="text-muted">...</h2> */

5. Design Token Organisation at Scale

Layer 1 — Primitive Tokens
--blue-100: oklch(95% 0.04 250) --blue-500: oklch(60% 0.22 250) --blue-900: oklch(20% 0.10 250) --space-1: 4px --space-2: 8px --space-4: 16px --space-8: 32px --radius-sm: 4px --radius-md: 8px --radius-full: 9999px --font-sans: system-ui, sans-serif --font-mono: 'Courier New', monospace
Rule: Raw values only. No semantic meaning. Never used directly in components.
Layer 2 — Semantic Tokens
--color-bg: var(--blue-950) --color-text: var(--blue-50) --color-surface: var(--blue-900) --color-accent: var(--blue-500) --color-text-muted: var(--blue-300) --color-border: var(--blue-800) --gap-sm: var(--space-2) --gap-md: var(--space-4) --gap-lg: var(--space-8)
Rule: References primitives. Carries intent. Only layer swapped for theming (dark/light/brand).
Layer 3 — Component Tokens
--btn-bg: var(--color-accent) --btn-color: #fff --btn-radius: var(--radius-md) --btn-padding: var(--space-2) var(--space-4) --card-bg: var(--color-surface) --card-radius: var(--radius-md) --input-border: var(--color-border) --input-focus-ring: var(--color-accent)
Rule: References semantics. Exposes component API. Overridden per variant, never per theme.
/* Token file organisation */ /* tokens/primitives.css — raw palette, never import directly in components */ :root { --blue-50: oklch(97% 0.02 250); --blue-500: oklch(60% 0.22 250); --blue-950: oklch(12% 0.06 250); } /* tokens/semantic.css — the theming surface */ :root { --color-bg: var(--blue-50); --color-text: var(--blue-950); --color-accent: var(--blue-500); } [data-theme="dark"] { --color-bg: var(--blue-950); --color-text: var(--blue-50); /* --color-accent stays the same */ } /* Multiband theming — brand + dark + high contrast */ [data-brand="enterprise"] { --color-accent: oklch(55% 0.18 150); /* green brand */ } @media (forced-colors: active) { :root { --color-bg: Canvas; --color-text: CanvasText; --color-accent: Highlight; } }

6. @layer Strategy for Teams

HIGH
@layer override
Utility classes, debug helpers, forced states. Single-property rules only.
utilities.css
MID
@layer theme
Semantic token overrides for dark mode, brand switching, high-contrast.
tokens/semantic.css
MID
@layer components
Card, button, nav, modal — BEM or component-scoped rules. Uses token variables.
components/*.css
LOW
@layer layout
Page templates, grid systems, flow primitives, sidebar layouts.
layout/*.css
LOW
@layer base
Element defaults, typography scale, reset rules, :root token declarations.
base.css · tokens/primitives.css
LOW
@layer vendor
Third-party libraries imported with @import … layer(vendor). Sandboxed.
@import … layer(vendor)
/* main.css — single entry point declares layer order */ @layer vendor, base, layout, components, theme, override; @import url('vendor/normalize.css') layer(vendor); @import url('tokens/primitives.css') layer(base); @import url('tokens/semantic.css') layer(base); @import url('base.css') layer(base); @import url('layout/grid.css') layer(layout); @import url('layout/flow.css') layer(layout); @import url('components/button.css') layer(components); @import url('components/card.css') layer(components); @import url('components/nav.css') layer(components); @import url('themes/dark.css') layer(theme); @import url('utilities.css') layer(override); /* Result: any utility class beats any component rule, no matter how specific the component selector was. */

7. File Structure Patterns

styles/ ├── tokens/ │ ├── primitives.css — raw palette, spacing, radii, fonts │ └── semantic.css — intent-named tokens + theme overrides │ ├── base/ │ ├── reset.css — modern CSS reset (box-sizing, margin 0) │ ├── typography.css — element defaults: h1-h6, p, a, code │ └── root.css — :root token declarations, @font-face │ ├── layout/ │ ├── grid.css — page grid, container, columns │ ├── flow.css — .stack, .cluster, .sidebar-layout │ └── prose.css — long-form content max-width, flow-space │ ├── components/ │ ├── button.css │ ├── card.css │ ├── nav.css │ ├── modal.css │ └── form.css │ ├── themes/ │ ├── dark.css — [data-theme="dark"] overrides │ └── brand-enterprise.css │ ├── utilities.css — single-property utility classes └── main.css — @layer order declaration + all @imports

8. Choosing a Methodology

Situation Recommended approach Why
Small team, no build step, shared templates (PHP/Jinja) BEM + @layer Flat specificity from naming, @layer handles cascade ordering
React/Vue SPA with component files CSS Modules or scoped CSS Tooling enforces encapsulation; no naming discipline needed
Design system with token infrastructure CUBE CSS + @layer + tokens Works with cascade, tokens are the API, @layer handles priority
Rapid prototyping, design constraint enforcement Tailwind utility-first Speed + built-in design constraints + excellent DX
Third-party CSS integration (Bootstrap, etc.) @import layer(vendor) + override layer Sandboxes vendor at lowest priority; override layer beats it cleanly
Content-heavy site, varying markup (CMS-driven) CUBE + flow/cluster primitives Composition primitives work on arbitrary markup; no class requirements on CMS content
Multi-brand / multi-theme product Three-tier token system + @layer theme Swap only semantic tokens per brand; components need no changes
The golden rule. Keep specificity flat. Every methodology that succeeds long-term does so because it makes high-specificity selectors the exception rather than the default. BEM does it with naming, utilities do it with single-class rules, CUBE does it by leaning on element selectors intentionally, and @layer does it by making layer position more important than selector weight. Pick the tool that fits your project — but don't skip the specificity discipline.

Chapter Summary

ConceptKey point
The core problemCSS is global by default. Architecture is the set of conventions that prevent styles bleeding into unintended elements and prevent specificity wars.
BEMBlock__Element--Modifier. All selectors are a single class (0-1-0). Modifiers pair with the block class: class="card card--featured". Works without tooling.
Utility-firstSingle-purpose classes composed in HTML. No cascade conflicts. Requires build tooling. Best for component-based JS frameworks.
CUBE CSSComposition (layout primitives) → Utility (overrides) → Block (components) → Exception (data attributes). Works with the cascade; uses custom properties as the component API.
CSS ModulesTooling scopes class names per file. True encapsulation without discipline. Global styles need :global() escape hatch. DevTools shows generated names.
Three-tier tokensPrimitives (raw palette) → Semantic (intent: --color-bg, --color-text) → Component (--btn-bg, --card-radius). Only swap semantic tokens for theming.
@layer for teamsDeclare order once in main.css. Import each file into its layer. Utility layer always beats component layer regardless of specificity — no more !important.
File structuretokens/ → base/ → layout/ → components/ → themes/ → utilities.css → main.css. main.css owns the @layer order and all @imports.
Vendor sandboxing@import url('bootstrap.css') layer(vendor) puts third-party CSS in the lowest-priority layer. Your own unlayered styles always win.
Choosing a methodNo single methodology wins universally. BEM for server-rendered teams, modules for JS SPAs, CUBE for design systems, Tailwind for rapid UI work. @layer + tokens applies to all of them.
Exercises
  1. BEM audit: Take an existing component stylesheet (your own or a public one). List every selector and its specificity score. Identify any rule above 0-2-0. Refactor those rules to BEM so all selectors are a single class. Verify in DevTools that specificity is flat throughout.
  2. Build a CUBE flow primitive: Implement .flow, .cluster, and .stack layout primitives. Apply them to a blog post layout: a .stack of sections, each section being a .cluster of cards, with .flow on prose content inside cards. Override spacing per-section with --flow-space and --stack-gap custom properties on individual elements.
  3. Three-tier token system: Create primitives.css, semantic.css, and use both in a button component and a card component. Add a [data-theme="dark"] rule to semantic.css that swaps --color-bg and --color-text. Toggle the attribute in the browser and confirm both components update with zero component CSS changes.
  4. @layer import chain: Create a main.css that declares @layer vendor, base, components, override. Import a small reset into vendor, write base typography into base, write a button component into components, and a .mt-0 utility into override. Apply .mt-0 to a button inside a deeply nested component and verify it wins without !important.
  5. Methodology comparison: Implement the same three-component UI (nav, card, button) three times: once in BEM, once in utility-first (write the classes by hand, no Tailwind needed), and once in CUBE CSS. Count the class names in the HTML, the lines in the CSS, and note which felt most natural. Write a short note on which you'd choose for a team project and why.
Next: Chapter 12 — Capstone Project. Build a complete dark-themed developer portfolio page from scratch — applying every concept from all twelve chapters: cascade layers, custom property token system, responsive fluid layout, accessible components, scroll-driven animations, and a production-ready file structure.