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> */
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 */@layervendor, base, layout, components, theme, override;@importurl('vendor/normalize.css')layer(vendor);@importurl('tokens/primitives.css')layer(base);@importurl('tokens/semantic.css')layer(base);@importurl('base.css')layer(base);@importurl('layout/grid.css')layer(layout);@importurl('layout/flow.css')layer(layout);@importurl('components/button.css')layer(components);@importurl('components/card.css')layer(components);@importurl('components/nav.css')layer(components);@importurl('themes/dark.css')layer(theme);@importurl('utilities.css')layer(override);/* Result: any utility class beats any component rule,
no matter how specific the component selector was. */
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
Concept
Key point
The core problem
CSS is global by default. Architecture is the set of conventions that prevent styles bleeding into unintended elements and prevent specificity wars.
BEM
Block__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-first
Single-purpose classes composed in HTML. No cascade conflicts. Requires build tooling. Best for component-based JS frameworks.
CUBE CSS
Composition (layout primitives) → Utility (overrides) → Block (components) → Exception (data attributes). Works with the cascade; uses custom properties as the component API.
CSS Modules
Tooling scopes class names per file. True encapsulation without discipline. Global styles need :global() escape hatch. DevTools shows generated names.
Three-tier tokens
Primitives (raw palette) → Semantic (intent: --color-bg, --color-text) → Component (--btn-bg, --card-radius). Only swap semantic tokens for theming.
@layer for teams
Declare order once in main.css. Import each file into its layer. Utility layer always beats component layer regardless of specificity — no more !important.
File structure
tokens/ → 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 method
No 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
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.
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.
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.
@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.
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.