CSS Architecture

🏗️ Chapter 11 — CSS Architecture: BEM and Utility-First Thinking

CSS has no enforced structure — you can name a class anything, nest selectors anywhere, and override anything with specificity. For a personal project, that freedom is fine. On a team or a codebase that grows over months, it becomes a liability: specificity wars, fear of deleting "dead" code that might secretly be used somewhere, and styles that only work because of the exact order they were written in. CSS architecture methodologies provide naming conventions and structural rules that tame this chaos.

1 — The Problem with Unstructured CSS

Imagine three developers separately adding styles for a button over six months:

🎨 CSS — the specificity war
/* Developer A, Month 1 */ button { background: blue; } /* 0,0,1 */ /* Developer B, Month 3 — needed it red in the sidebar */ .sidebar button { background: red; } /* 0,1,1 */ /* Developer C, Month 6 — needs it blue again in sidebar */ .sidebar .btn { background: blue !important; } /* nuclear option */ /* Now nobody can safely change anything without checking every combination of .sidebar, button, .btn, !important... */

The result is CSS that only works because of its order and because of implicit knowledge of what elements exist in the HTML. Architecture methodologies eliminate these problems before they start.

Specificity scores of common selectors

.button0,1,0
BEM — single class, flat and safe
div.button0,1,1
Tag qualifier — harder to override
.sidebar .button0,2,0
Context dependency — breaks when HTML changes
.nav .sidebar .button0,3,0
Deep nesting — brittle, hard to test
#header .nav .button1,2,0
ID selector — almost impossible to override cleanly
.button { color: red !important; }
!important — last resort, causes escalation

2 — BEM: Block Element Modifier

BEM, developed at Yandex in 2005, solves the specificity problem with a strict naming convention. Every CSS rule targets exactly one class, keeping specificity at 0,1,0 everywhere. The naming convention encodes the component structure into the class names themselves, making the HTML self-documenting.

The three parts

🎨 CSS — BEM naming rules
/* BLOCK — a standalone, reusable component */ .card { } /* the card as a whole */ .nav { } .button { } .modal { } /* ELEMENT — a child of a block, separated by __ (double underscore) */ .card__image { } /* .card's image */ .card__title { } .card__footer { } .nav__item { } .modal__overlay { } /* MODIFIER — a variant or state, separated by -- (double dash) */ .card--featured { } /* a featured card */ .card--compact { } /* a smaller card */ .button--primary { } .button--disabled{ } .nav__item--active{ } /* element modifier */
A modifier is always used in addition to the base class: <div class="card card--featured"> — not <div class="card--featured"> alone. The modifier only overrides the parts that change.
BEM Anatomy Explorer
Technology

How CSS Architecture Saves Teams

Naming conventions turn chaotic stylesheets into predictable, maintainable systems.

Blue outline = Block. Purple dashed = Elements (with class names). Modifier buttons show visual variants.

BEM rules and anti-patterns

🎨 CSS — BEM: good vs bad
/* ✓ GOOD — flat specificity, predictable */ .card { border-radius: 8px; } /* 0,1,0 */ .card__title { font-size: 1.2rem; } /* 0,1,0 */ .card--featured { border-color: gold; } /* 0,1,0 */ /* ✗ ANTI-PATTERN: nesting element inside block selector */ .card .card__title { } /* specificity is now 0,2,0 — harder to override */ /* ✗ ANTI-PATTERN: three levels deep */ .card__body__paragraph { } /* wrong — use .card__text instead */ /* ✗ ANTI-PATTERN: element selector inside BEM */ .card h2 { } /* breaks when h2 becomes h3 or a div */ /* ✗ ANTI-PATTERN: BEM + utility chaos */ .card--featured.is-active.has-image { } /* two different systems colliding */ /* ✓ GOOD: The MIX pattern — one element, two blocks */ /* <div class="card layout__item"> */ /* .card controls the card's appearance */ /* .layout__item controls how it sits in its container */
🏷️ HTML — BEM markup in practice
<!-- Navigation component --> <nav class="nav"> <ul class="nav__list"> <li class="nav__item"> <a class="nav__link" href="/">Home</a> </li> <li class="nav__item nav__item--active"> <a class="nav__link" href="/about">About</a> </li> <li class="nav__item"> <a class="nav__link nav__link--cta" href="/contact">Contact</a> </li> </ul> </nav> <!-- The HTML alone tells you: nav (block), nav__item (elements), --active and --cta (modifiers). No reading the CSS file needed. -->

3 — Utility-First CSS

Utility-first takes the opposite approach: instead of naming components and writing CSS for them, you compose dozens of single-purpose classes directly in the HTML. Every class does exactly one thing: .flex, .p-4, .text-blue-500, .rounded-lg, .hover:shadow-md. Tailwind CSS popularised this approach and is now the most-downloaded CSS framework.

🏷️ HTML — BEM vs utility-first: same button
<!-- BEM — semantic class name, styling in CSS file --> <button class="button button--primary button--large"> Submit </button> <!-- Utility-first (Tailwind) — styling in HTML itself --> <button class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold text-base hover:bg-blue-700 active:scale-95 transition-all"> Submit </button> <!-- Tailwind component abstraction — best of both: extract to a component in your JS framework --> <PrimaryButton>Submit</PrimaryButton> <!-- The component internally uses utilities but hides them -->
Button Variants — BEM modifier approach
<button class="button button--primary">Primary</button> <button class="button button--secondary">Secondary</button> <button class="button button--danger">Danger</button> <button class="button button--ghost">Ghost</button>

Writing your own utility classes

🎨 CSS — a simple utility layer
/* Spacing utilities (u- prefix marks utility) */ .u-mt-0 { margin-top: 0; } .u-mt-1 { margin-top: 0.5rem; } .u-mt-2 { margin-top: 1rem; } .u-mt-4 { margin-top: 2rem; } /* Display utilities */ .u-flex { display: flex; } .u-grid { display: grid; } .u-hidden{ display: none; } /* Text utilities */ .u-truncate { overflow: hidden; text-overflow:ellipsis; white-space: nowrap; } .u-sr-only { /* screen-reader only */ position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0); overflow: hidden; }

4 — Other Methodologies (Overview)

OOCSS
Object-Oriented CSS — Nicole Sullivan, 2009
Two rules: separate structure from skin (layout vs appearance) and separate container from content (styles not tied to location). Predates BEM; introduced the "object" mental model.
SMACSS
Scalable and Modular Architecture for CSS — Jonathan Snook, 2011
Organises rules into five categories: Base (resets), Layout (l- prefix), Module (components), State (is- prefix), Theme. Good for large projects.
ITCSS
Inverted Triangle CSS — Harry Roberts
Orders CSS from generic to specific in layers: Settings → Tools → Generic → Elements → Objects → Components → Utilities. Specificity only increases, preventing cascade conflicts.
CUBE CSS
Composition Utility Block Exception — Andy Bell, 2020
A modern hybrid: lean into the cascade (C), use utility classes (U), define blocks for components (B), handle exceptions with modifiers (E). Embraces CSS rather than fighting it.

5 — The Hybrid Approach (Most Practical)

Most real projects land somewhere between pure BEM and pure utility-first. A practical hybrid: BEM for components, utilities for layout and one-offs.

🏷️ HTML — hybrid: component + utility helpers
<!-- Component class handles the card's appearance --> <!-- Utility classes handle spacing, layout, one-offs --> <article class="card card--featured u-mt-4"> <h2 class="card__title u-truncate">Long Title That Gets Cut Off</h2> <p class="card__excerpt">Excerpt...</p> </article> <!-- Layout shell is utility-driven --> <div class="u-grid u-grid-cols-3 u-gap-4"> <article class="card">…</article> <article class="card card--featured">…</article> </div>

6 — CSS Custom Properties as Architecture

Regardless of which naming methodology you choose, CSS custom properties (variables) provide the foundation that makes any system consistent. Design tokens — the atoms of a design system — live in :root and are consumed everywhere.

🎨 CSS — design tokens in :root
/* Layer 1: Raw values (primitive tokens) */ :root { /* Color palette */ --blue-500: #4f8ef7; --blue-600: #3a7ae5; --gray-100: #f3f4f6; --gray-900: #111827; /* Layer 2: Semantic tokens (what things ARE) */ --color-primary: var(--blue-500); --color-primary-hover:var(--blue-600); --color-surface: var(--gray-100); --color-text: var(--gray-900); /* Spacing scale */ --space-1: clamp(0.5rem, 1vw, 0.75rem); --space-2: clamp(1rem, 2vw, 1.5rem); --space-4: clamp(2rem, 4vw, 3rem); /* Typography */ --text-base: clamp(1rem, 1.5vw + 0.25rem, 1.25rem); --text-lg: clamp(1.25rem, 2.5vw, 1.75rem); /* Component tokens (consume semantic tokens) */ --card-padding: var(--space-2); --card-radius: 8px; --card-border: 1px solid var(--color-border); } /* Dark mode — just re-map the semantic tokens */ @media (prefers-color-scheme: dark) { :root { --color-surface: var(--gray-900); --color-text: var(--gray-100); /* --blue-500 stays the same — only semantic tokens change */ } } /* Components reference tokens — never raw values */ .card { background: var(--color-surface); color: var(--color-text); padding: var(--card-padding); border-radius:var(--card-radius); } /* Swap themes by re-mapping :root tokens — no component CSS changes */

7 — Choosing an Approach

ContextRecommendationWhy
Small personal projectWhatever feels naturalArchitecture pays off at scale, not worth the overhead on solo projects
Team project (no framework)BEM + design tokensPredictable naming prevents cross-developer conflicts
React / Vue / Svelte projectCSS Modules or utility frameworkComponent scoping solves the global namespace problem; Tailwind integrates naturally
Large-scale site / CMSITCSS or SMACSS + BEMLayer-based architecture handles hundreds of components cleanly
Design system / component libraryDesign tokens + BEMTokens provide consistency; BEM makes components self-contained
Rapid prototypingUtility-first (Tailwind)No context switching between HTML and CSS — fastest iteration speed
💡 The rule that matters most: Pick one system and apply it consistently. A project using BEM consistently for six months is vastly better than one that uses BEM in some files, SMACSS in others, and no methodology in the rest. Consistency is more valuable than choosing the "best" methodology.

8 — Quick Reference

ConceptSyntaxRule
Block.cardStandalone reusable component
Element.card__titlePart of a block, double underscore
Modifier.card--featuredVariant or state, double dash — always used alongside base class
Element modifier.card__title--truncatedState of an element — keep depth to two levels maximum
MIXclass="card layout__cell"One HTML element plays two roles — different concerns, different classes
Utility.u-truncateSingle-purpose class; prefix to distinguish from components
Design token--color-primarySemantic custom property; always reference tokens not raw values

✏️ Exercises

Good architecture is a habit, not a one-time choice. These exercises build the muscle memory of naming things intentionally.

Exercise 1
Name every element in a blog post component using BEM. The component contains: the outer article wrapper, a hero image, a category badge (on top of the image), the content area, a heading, a meta row (author name + date + read time), the body text, a tags row (list of tag pills), and a share buttons row. Write only the class names — the CSS can be blank. Use modifiers where relevant (featured post, pinned post).
Hint: Start with the block name (e.g. .post). Elements use .post__*. Go only two levels deep — if a tag pill contains an icon, that icon is .post__tag-icon not .post__tags__tag__icon.
HTML (class names only)
<article class="post post--featured"> <div class="post__hero"> <img class="post__image"> <span class="post__category">Technology</span> </div> <div class="post__content"> <h1 class="post__title">Title</h1> <div class="post__meta"> <span class="post__author">Jane</span> <time class="post__date">Jun 2026</time> <span class="post__read-time">5 min</span> </div> <div class="post__body">…</div> <ul class="post__tags"> <li class="post__tag">CSS</li> <li class="post__tag post__tag--highlight">Architecture</li> </ul> <div class="post__share"> <button class="post__share-btn">Twitter</button> <button class="post__share-btn">LinkedIn</button> </div> </div> </article>
Exercise 2
Convert this "messy CSS" into BEM. The original uses tag selectors, ID selectors, context-dependent rules, and !important. Rewrite it as flat BEM rules — all targeting a single class at 0,1,0 specificity. Add modifier rules where the original had context-based overrides.
Hint: Remove all ID selectors, element selectors, and descendant/child combinators. Every selector should be a single class.
Before (messy)
/* ✗ Original messy CSS */ #header nav ul li a { color: white; } #header nav ul li.active a{ color: gold !important; } .sidebar nav ul li a { color: black; }
After (BEM)
/* ✓ BEM rewrite */ .nav__link { color: white; } .nav__link--active { color: gold; } .nav--sidebar .nav__link { color: black; } /* Or even better — use a modifier on the link itself: */ .nav__link--dark { color: black; } /* HTML: <a class="nav__link nav__link--dark"> in sidebar context */
Exercise 3
Design a design token system for a simple project. Define: (a) a palette of 3 brand colours with 3 shades each in numbered custom properties; (b) semantic tokens mapping those to purpose (--color-primary, --color-danger, --color-surface, --color-text, --color-border); (c) a dark-mode override using @media (prefers-color-scheme: dark) that only re-maps the semantic tokens; (d) a spacing scale using clamp() from --space-1 to --space-6.
Hint: The primitive tokens (raw colors) stay the same in dark mode. Only the semantic tokens change — --color-surface flips from a light to dark shade.
CSS
/* (a) Primitive palette */ :root { --blue-300: #7ab8ff; --blue-500: #4f8ef7; --blue-700: #2a5ab0; --red-300: #f08080; --red-500: #e05555; --red-700: #a02020; --gray-100: #f3f4f6; --gray-500: #6b7280; --gray-900: #111827; /* (b) Semantic tokens — light mode defaults */ --color-primary: var(--blue-500); --color-primary-hover:var(--blue-700); --color-danger: var(--red-500); --color-surface: var(--gray-100); --color-text: var(--gray-900); --color-text-muted: var(--gray-500); --color-border: var(--gray-500); /* (d) Fluid spacing scale */ --space-1: clamp(0.25rem, 0.5vw, 0.5rem); --space-2: clamp(0.5rem, 1vw, 0.875rem); --space-3: clamp(0.875rem,1.5vw, 1.25rem); --space-4: clamp(1.25rem, 2.5vw, 2rem); --space-5: clamp(2rem, 4vw, 3rem); --space-6: clamp(3rem, 7vw, 5rem); } /* (c) Dark mode — remap semantic tokens only */ @media (prefers-color-scheme: dark) { :root { --color-primary: var(--blue-300); /* lighter in dark mode */ --color-surface: var(--gray-900); --color-text: var(--gray-100); --color-border: #2a2d3a; /* Primitive palette unchanged — only semantics flip */ } }
Exercise 4
Audit a real CSS file (any file from a previous exercise or your own project) and identify: (a) any selectors with specificity higher than 0,1,0; (b) any use of !important; (c) any selectors that depend on HTML structure (descendant selectors); (d) any duplicate property declarations that differ only in context. Write a refactored version of the most problematic section using either BEM naming or design tokens, and explain in a comment block at the top why each change was made.
Hint: DevTools > Elements > Styles shows specificity if you hover over selectors. Or use the Specificity Calculator at specificity.keegan.st. Look for rules where you had to add more selectors "to make it win."
CSS — example refactor
/* AUDIT FINDINGS: ✗ .sidebar .nav li a — specificity 0,2,2, context-dependent ✗ #main .btn — specificity 1,1,0, ID makes it unmatchable ✗ !important on color — symptom of a specificity war ✗ margin repeated 4 times for different contexts REFACTOR DECISIONS: - Replaced .sidebar .nav li a with .nav__link--sidebar (0,1,0) - Replaced #main .btn with .btn--main-variant (0,1,0) - Removed !important by fixing the selector chain - Extracted margin to a --card-gap token, used everywhere */ /* Before */ #sidebar .nav li a { color: blue !important; } /* After */ :root { --nav-link-sidebar-color: var(--color-primary); } .nav__link--sidebar { color: var(--nav-link-sidebar-color); }