Topic 11.4
Organising CSS
In one line
Large CSS stays manageable when every rule has an obvious home and predictable strength: a reset or normalise base, components named with a convention like BEM or scoped by a tool, optional utility classes, and cascade layers that fix the order between them.
Think of it like this
A kitchen. In a tidy kitchen, plates live in one cupboard, spices on one shelf, and knives in one drawer. Anyone can find anything and put it back. In a messy kitchen, the salt might be in three places and nobody dares throw anything away. CSS is the same: without rules about where styles live and how they are named, every new feature adds more rules that fight the old ones.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Global (in CSS)
- Any rule can match any element anywhere on the site, unless something limits it.
- Specificity war
- Making selectors stronger and stronger to override each other, often ending in
!important. - Reset
- A stylesheet that removes the browser's default styles so every element starts plain.
- Normalise
- A stylesheet that keeps useful browser defaults but makes them the same in every browser.
- BEM
- Block, Element, Modifier: a class naming convention like
.card,.card__title,.card--featured. - Utility class
- A tiny class that does one thing, like
.text-centeror.p-4. - CSS Modules
- A build-time tool that renames your classes to unique names so each component's CSS can't leak.
- Scoped styles
- Styles that a tool or the browser limits to one component.
- Cascade layer
- A named group of rules (
@layer components) whose order beats specificity.
Step by step
01Start from a small modern reset
Put the reset first so every later rule builds on the same base in every browser.
*, *::before, *::after { box-sizing: border-box; }
body, h1, h2, h3, p, figure { margin: 0; }
body { line-height: 1.5; }
img, svg, video { display: block; max-width: 100%; }
input, button, select, textarea { font: inherit; }02Name components with BEM
One block, its elements with __, its variants with --. Every selector is one class, so they all have the same strength and order decides between them.
.card { padding: 16px; border: 1px solid #e5e7eb; }
.card__title { font-size: 1.25rem; }
.card__body { color: #4b5563; }
.card--featured { border-color: #6366f1; }03Write the matching HTML
A modifier class is added next to the block class, never alone, because it only changes what the block already set.
<article class="card card--featured">
<h2 class="card__title">Pro plan</h2>
<p class="card__body">Everything in Basic, plus more.</p>
</article>04Add a few utilities
Utilities handle one-off spacing and alignment so you don't create a modifier for every small difference.
.mt-0 { margin-top: 0; }
.text-center { text-align: center; }
.visually-hidden { /* Topic 11.7 */ }05Order everything with layers
Declare the layer order once at the top. From then on, a utility always beats a component, and a component always beats the reset, regardless of selectors.
@layer reset, base, components, utilities;
@import url("reset.css") layer(reset);
@import url("card.css") layer(components);
@import url("utilities.css") layer(utilities);Try it yourself
- 1
Add a modifier
In the first example, add a
card--mutedmodifier that greys out the border and text. Predict what the console prints for a card with bothcardandcard--muted, then add it to the HTML and run. - 2
Swap the layer order
In the second example, change the first line to
@layer reset, utilities, components;. Predict the colour of the second title. Run it. - 3
Unlayered beats layered
In the second example, add
h2 { color: green; }at the bottom, outside any layer. Predict both titles' colours before running.
Code & diagrams
The modifier only changes what's different. Notice the one exception to "one class per selector": a modifier styling its own elements (`.card--featured .card__title`), which BEM allows.
Preview
Console
Nothing logged yet.Expected console output
card -> rgb(229, 231, 235)
card card--featured -> rgb(99, 102, 241)Layer order is checked before specificity, so `.text-red` (0,1,0) beats `#pricing .card .title` (1,2,0). The unlayered `.card` rule at the bottom would beat all three layers if they set the same property.
Preview
Console
Nothing logged yet.Expected console output
component: rgb(0, 0, 128)
with utility: rgb(220, 20, 60)
h2 margin from reset: 0pxEach class does one job, and the HTML describes the look. This is the idea behind Tailwind, which generates classes like these for you (Topic 11.5).
Preview
Console
Nothing logged yet.Expected console output
gap: 12px
padding: 16px/* Card.module.css */
/* .title { font-weight: 700; } */
// Card.jsx
import styles from "./Card.module.css";
export function Card({ title }) {
// styles.title is a unique generated name, e.g. "Card_title__x7a2"
return <h2 className={styles.title}>{title}</h2>;
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A modifier used on its own
In the first example, change the second card's classes to only card--featured (remove card).
Break #2
Forget that unlayered styles win
In the second example, move .text-red { color: crimson; } out of @layer utilities and add an unlayered .card .title { color: navy; } below it.
Myth vs fact
Myth
BEM's long class names are bad for performance.
Fact
Class name length barely matters, and single-class selectors are among the fastest to match. Gzip and Brotli compression also shrink repeated names to almost nothing.
Myth
Utility-first means inline styles with extra steps.
Fact
Utilities can use media queries, hover states, design tokens and a fixed scale of values, which inline styles can't; and they are cached as one stylesheet.
Myth
A reset is always better than normalise.
Fact
A full reset removes useful defaults (list bullets, heading sizes) you then rebuild. Most projects today use a small modern reset that keeps sensible defaults: a mix of both ideas.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The order of layers is fixed by the first time each name appears, so put one
@layer a, b, c;statement at the very top. Third-party CSS can be put into a low layer with@import url(lib.css) layer(vendor);to make it easy to override. - ▸
For
!importantdeclarations, layer order is reversed: an important declaration in an earlier layer beats one in a later layer. That lets a reset protect something like[hidden] { display: none !important; }against everything else. - ▸
@scope (.card) to (.card__content) { ... }limits rules to a part of the tree and stops at a lower boundary ("donut scope"). It is supported in Chromium and Safari; check current Firefox support before relying on it. - ▸
Measure dead CSS with DevTools' Coverage panel (it shows which bytes of each stylesheet were used during your session). It can't prove a rule is unused on other pages or states, so combine it with naming that shows where styles belong.
Remember this
- 1
The core problem is that CSS is global: any rule can match any element on any page. In a large project that causes specificity wars (rules made stronger and stronger to win, ending in
!important), dead code nobody dares delete because they can't tell what uses it, and leaks where a style for one page breaks another. Every architecture idea below tackles these three problems. - 2
A reset removes browser default styles so you start from zero (Eric Meyer's classic reset set margins, padding and font sizes to 0 or inherit for nearly everything). A normalise stylesheet (normalize.css) keeps useful defaults but makes them consistent across browsers and fixes their bugs. Modern projects usually use a small modern reset:
box-sizing: border-boxeverywhere,margin: 0on body and headings,img { display: block; max-width: 100% },font: inheriton form controls, and sensibleline-height. It belongs in the lowest layer of your CSS. - 3
BEM (Block, Element, Modifier) is a naming convention: a set of rules for class names. A block is a standalone component (
.card). An element is a part that only makes sense inside it, written with two underscores (.card__title). A modifier is a variant or state, written with two dashes (.card--featured,.card__title--large). Every selector is a single class, so every rule has the same low specificity (0,1,0), names show where styles live, and the page structure can change without breaking selectors. - 4
Utility-first CSS flips the idea: instead of naming components, you use many tiny classes that each do one thing (
.flex,.p-4,.text-center,.rounded-lg) and compose them in the HTML. Tailwind CSS (Topic 11.5) is the best-known example. Pros: you rarely write new CSS, the stylesheet stops growing, and nothing leaks. Cons: long class lists in the HTML, and repeated combinations need a component (in your framework) to stay consistent. - 5
Component styles use tools to scope CSS to one component automatically. CSS Modules rename
.titleinCard.module.cssto something unique likeCard_title__x7a2at build time, so two components can both have.title. Vue's<style scoped>and Svelte do similar things with attributes. Shadow DOM (Web Components) gives true isolation in the browser itself. These solve leaks without a naming convention. The React course (Phase 11) compares these options for React apps, including Tailwind and CSS-in-JS. - 6
Cascade layers (
@layer, Phase 2) fix the order of whole groups of rules, whatever their specificity. A common setup:@layer reset, base, components, utilities;. A rule in a later layer beats any rule in an earlier layer, so a single-class utility can override a heavy component selector without!important. Styles not in any layer beat all layered styles, which is handy for quick overrides and surprising if you forget it. - 7
Whatever you pick, decide it once and write it down: file structure (one file per component, plus
tokens,reset,base,utilities), naming, the maximum nesting depth, and where tokens live. Consistency matters more than which system you choose. Many real projects combine them: tokens plus a modern reset, components with BEM or CSS Modules, a few utilities, all ordered by layers.
Explain it without notes
Why does CSS become hard to maintain in large projects?
Explain BEM with an example. What problems does it solve?
Compare utility-first CSS with component classes. When would you choose each?
What is the difference between a reset and normalize.css? What does a modern reset include?
How do cascade layers help organise a CSS codebase?
Practice
Write BEM CSS and HTML for an alert with a message and a close button, and a danger variant.
Set up layers so a reset, a component and a utility all style h2, and the utility wins even though the component's selector is more specific.
Trade-offs
- ↔
Naming conventions like BEM need discipline but no tools; scoped styles (CSS Modules, Shadow DOM) need a build step or Web Components but enforce isolation automatically.
- ↔
Utility-first speeds up building and keeps CSS small, but moves design decisions into long class lists in the markup.
- ↔
Layers end specificity wars, but forgetting that unlayered CSS beats every layer creates new surprises; agree on what stays unlayered.
Done when you can
I can explain why global CSS gets hard to maintain.
I can name classes with BEM: block, element, modifier.
I can compare utility-first and component CSS.
I know the difference between a reset, normalise and a modern reset.
I can order a codebase with
@layer, including what unlayered styles do.