Command Palette

Search for a command to run...

Hectal
PHASE 11Advanced ~22 min· topic 4 of 8

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-center or .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.

reset.csswhole filecss
*, *::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.csswhole filecss
.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.

index.htmlwhole filehtml
<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.

utilities.csswhole filecss
.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.

main.csswhole filecss
@layer reset, base, components, utilities;
@import url("reset.css") layer(reset);
@import url("card.css") layer(components);
@import url("utilities.css") layer(utilities);
Order everything with layersdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a modifier

    In the first example, add a card--muted modifier that greys out the border and text. Predict what the console prints for a card with both card and card--muted, then add it to the HTML and run.

  2. 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. 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

A BEM component New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

card -> rgb(229, 231, 235)
card card--featured -> rgb(99, 102, 241)
Layers: a utility beats a stronger component selector New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

component: rgb(0, 0, 128)
with utility: rgb(220, 20, 60)
h2 margin from reset: 0px
Utility-first: composing small classes New tab

Each 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

gap: 12px
padding: 16px
CSS Modules: scoped class names (needs a build tool, not live)javascript
/* 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).

terminal
$ Console
── what you'll see ──
card--featured -> rgb(99, 102, 241), but the card has lost its padding and rounded corners on the page.

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.

terminal
$ Console
── what you'll see ──
with utility: rgb(0, 0, 128)

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 !important declarations, 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. 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. 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-box everywhere, margin: 0 on body and headings, img { display: block; max-width: 100% }, font: inherit on form controls, and sensible line-height. It belongs in the lowest layer of your CSS.

  3. 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. 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. 5

    Component styles use tools to scope CSS to one component automatically. CSS Modules rename .title in Card.module.css to something unique like Card_title__x7a2 at 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. 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. 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

01

Why does CSS become hard to maintain in large projects?

02

Explain BEM with an example. What problems does it solve?

03

Compare utility-first CSS with component classes. When would you choose each?

04

What is the difference between a reset and normalize.css? What does a modern reset include?

05

How do cascade layers help organise a CSS codebase?

Practice

01

Write BEM CSS and HTML for an alert with a message and a close button, and a danger variant.

02

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.