CSSAdvanced

CSS Architecture: BEM Naming and Organising Stylesheets

Learn BEM naming, low-specificity selectors, and stylesheet organisation to keep large CSS projects clean, scalable, and maintainable.

All CSS lessons

What you will learn

A small stylesheet is easy to manage. A stylesheet that has grown to 3,000 lines, been edited by five people over two years, is not. Rules fight each other, nobody dares to delete anything, and every change breaks something unexpected. CSS architecture is the set of habits that prevents this: how you name things, how you keep specificity low, and how you organise files. In this lesson you will learn the most popular naming method, BEM, the rules that keep selectors flat and predictable, and a folder and layer structure you can use on real projects.

Why large CSS goes wrong

Problem What it looks like
Name collisions Two developers both create .title, and a style written for the blog now breaks the shop
Specificity wars .sidebar .widget h3 needs .page .sidebar .widget h3 to override it, which needs !important
Coupling to HTML .card > div > h2 breaks as soon as someone adds a wrapper <div>
Dead code Nobody knows if .old-banner is still used, so it stays forever
Hard to find Is the button’s hover style in the header section, the forms section or the “misc” section?

Good architecture attacks all five with a few simple rules.

The HTML we will style

The practice page is built with BEM names. Read the class names carefully; they tell you what belongs to what.

<header class="site-header">
  <a class="site-header__logo" href="#home">CodeNest</a>
  <nav class="nav" aria-label="Main">
    <ul class="nav__list">
      <li class="nav__item"><a class="nav__link nav__link--active" href="#home">Home</a></li>
      <li class="nav__item"><a class="nav__link" href="#courses">Courses</a></li>
      <li class="nav__item"><a class="nav__link" href="#about">About</a></li>
    </ul>
  </nav>
</header>

<main class="page">
  <article class="card">
    <h2 class="card__title">Regular card</h2>
    <p class="card__text">Each part of the card has its own class...</p>
    <div class="card__actions">
      <a class="btn btn--primary" href="#start">Start</a>
      <button class="btn btn--ghost" type="button">Save</button>
    </div>
  </article>

  <article class="card card--featured">
    <h2 class="card__title">Featured card</h2>
    <p class="card__text">One modifier class changes the whole card...</p>
    <div class="card__actions">
      <a class="btn btn--primary btn--small" href="#start">Start</a>
      <button class="btn btn--ghost btn--small" type="button">Save</button>
    </div>
  </article>
</main>
Class Element BEM role
.site-header the <header> block
.site-header__logo the logo <a> element of site-header
.nav the <nav> block
.nav__list, .nav__item, .nav__link the <ul>, each <li> and each <a> elements of nav
.nav__link--active the “Home” link modifier of the nav__link element
.page the <main> a layout block
.card each <article> block
.card__title, .card__text, .card__actions the <h2>, the <p>, the <div> elements of card
.card--featured the second <article> modifier of the card block
.btn the links and buttons in the card block (a separate, reusable component)
.btn--primary, .btn--ghost, .btn--small each button modifiers of btn

BEM: Block, Element, Modifier

BEM is a naming convention with three ideas:

  • Block: a standalone, reusable component that makes sense on its own: card, nav, btn, site-header.
  • Element: a part of a block that has no meaning outside it, written block__element (two underscores): card__title, nav__link.
  • Modifier: a variation or state of a block or element, written block--modifier (two hyphens): card--featured, btn--small, nav__link--active.
.card                  <- block
.card__title           <- element:  a part OF the card
.card__actions         <- element
.card--featured        <- modifier: a different VERSION of the card

The rules of BEM

  1. Use one class per thing, and style by class only. Every selector is a single class: specificity stays at 0-1-0, so rules never fight.
  2. A modifier is added next to the base class, never instead of it:
<a class="btn btn--primary">   <!-- good: btn gives the shared look, btn--primary adds colour -->
<a class="btn--primary">       <!-- bad: it has nothing to modify -->
  1. Elements belong to the block, not to each other. Do not chain: .card__actions__button is wrong. Name it for the block it belongs to: .card__button. Elements can be nested deeply in the HTML, but their names stay flat.
  2. Name by purpose, not appearance. .card__title stays correct when the title turns green. .big-red-text becomes a lie the day the design changes.
  3. Different components get different blocks. A button inside a card is not a “card element”. It is a separate btn block that the card happens to contain. That is why the card uses class="btn btn--primary" and not card__button.
  4. Use lower case with hyphens inside a name: site-header, nav__link.

Writing the CSS

Each BEM name becomes one flat selector:

/* --- Block: card --- */
.card {
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}

/* Elements of the card */
.card__title {
  margin: 0 0 var(--space-1);
}

.card__text {
  margin: 0 0 var(--space-2);
  color: #475569;
}

.card__actions {
  display: flex;
  gap: var(--space-1);
}

/* Modifier: a variation of the whole card */
.card--featured {
  border-color: var(--color-primary);
  background-color: #eff6ff;
}

Notice there is no .card h2, no .card > p, and no descendant selectors. The CSS is not tied to the HTML tags. A developer can change the <h2> to an <h3> (for correct heading order) and nothing breaks.

A modifier on an element

.nav__link {
  display: block;
  padding: var(--space-1) var(--space-2);
  color: #cbd5e1;
  text-decoration: none;
}

/* Modifier: the link of the current page */
.nav__link--active {
  color: white;
  background-color: var(--color-primary);
  border-radius: 999px;
}

A reusable block with several modifiers

The btn block is used on its own and inside the card. Its modifiers add colour and size:

.btn {
  display: inline-block;
  padding: 0.6rem 1.1rem;
  border: 2px solid transparent;
  border-radius: 8px;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.btn--primary {
  background-color: var(--color-primary);
  color: white;
}

.btn--primary:hover {
  background-color: var(--color-primary-dark);
}

.btn--ghost {
  background-color: transparent;
  color: var(--color-primary);
  border-color: var(--color-primary);
}

.btn--small {
  padding: 0.35rem 0.8rem;
  font-size: 0.875rem;
}

Because modifiers are independent, they combine: btn btn--primary btn--small is a small primary button, with no new CSS. That is the real benefit of the system.

Native nesting and BEM

You can use nesting for states and pseudo-classes, but remember that &__title and &--featured do not work in native CSS (see the nesting lesson). Write the full names, and use nesting only where it helps:

.btn--primary {
  background-color: var(--color-primary);
  color: white;

  &:hover {
    background-color: var(--color-primary-dark);
  }
}

Is BEM too verbose?

Yes, the names are long, and that is the price. In return you get predictable names, no collisions, flat specificity and CSS you can search for with Ctrl + F. Other popular approaches exist: utility-first CSS (small single-purpose classes such as p-4 and text-center, as in Tailwind), CUBE CSS, SMACSS and ITCSS. They differ in style but share the same goals. The important thing is to choose one convention and apply it consistently across a project.

State classes and ARIA

For things that change at run time (open, active, selected), you have two options:

/* 1. a state class that JavaScript adds and removes */
.nav__link.is-active { ... }

/* 2. an attribute that already says the same thing to assistive technology */
.nav__link[aria-current="page"] { ... }
.menu-button[aria-expanded="true"] { ... }

Prefer the ARIA attribute when one exists: you must set it anyway for accessibility, and styling from it guarantees that what people see and what screen readers announce can never get out of sync. Use is- / has- classes for states that ARIA does not cover.

Rules for keeping the CSS healthy

  • Style with classes only. Avoid ids (too strong) and bare tags in components (.card h2 couples CSS to HTML).
  • Keep specificity flat. Aim for 0-1-0 almost everywhere. Use :where() for resets.
  • Do not use !important except in a utility class.
  • Never style by position in the document (div:nth-child(3) > span).
  • One component, one place. All the rules of .card live together.
  • Use variables for every repeated value (colours, spacing, radius).
  • Add comments for the “why”, not the “what”. /* min-width: 0 stops the long URL stretching the grid */ is useful. /* sets the colour */ is noise.
  • Delete what you do not use. In Chrome, the DevTools Coverage panel shows which CSS was never applied on a page.
  • Add a linter. Tools like Stylelint catch duplicate properties, typos and invalid values automatically, and a formatter such as Prettier keeps the layout consistent.

Organising the files

Inside one file

For a small site, one style.css with clearly marked sections is perfect. Start it with a table of contents (like the one in the practice editor) and keep the sections in this order, from the most general to the most specific:

  1. Settings: :root variables.
  2. Base: the reset and defaults for plain elements (body, a, h1).
  3. Layout: the page structure (.page, .container, grids).
  4. Components: each block (.card, .btn, .nav), one section per block.
  5. Utilities: small single-purpose helpers (.visually-hidden, .text-center) that must win.

This order works with the cascade: later rules win ties, so the most specific, most intentional styles come last. It is the same order you would give to @layer (see below).

Across several files

When a file passes roughly 500 lines, split it by those same groups:

css/
├── main.css                 <- declares the layer order and imports the rest
├── settings/
│   └── tokens.css           <- :root { --color-primary: ... }
├── base/
│   ├── reset.css
│   └── typography.css
├── layout/
│   ├── container.css
│   └── grid.css
├── components/
│   ├── button.css           <- everything about .btn
│   ├── card.css             <- everything about .card
│   └── nav.css              <- everything about .nav
└── utilities.css

One file per component means that “where is the card style?” has one answer, and that deleting a component means deleting one file.

Using @layer for the structure

Cascade layers turn the order above from a convention into a guarantee:

/* main.css */
@layer settings, reset, base, layout, components, utilities;

@import url("settings/tokens.css") layer(settings);
@import url("base/reset.css") layer(reset);
@import url("base/typography.css") layer(base);
@import url("layout/container.css") layer(layout);
@import url("components/button.css") layer(components);
@import url("components/card.css") layer(components);
@import url("utilities.css") layer(utilities);

Now utilities always beats components, no matter how the selectors compare. A word of warning: @import makes the browser download files one after another, which slows a real website. For production, combine the files into one with a build tool (Vite, PostCSS, or the one built into your framework). The next lesson explains why.

An anatomy for every component file

A short header comment makes every component easy to understand:

/* ==========================================================
   COMPONENT: card
   Purpose:   a boxed piece of content with title, text and actions
   Elements:  card__title, card__text, card__actions
   Modifiers: card--featured
   Used in:   course list, home page
   ========================================================== */
@layer components {
  .card { ... }
  .card__title { ... }
  /* ... */
}

Putting it together

/* 1. SETTINGS */
:root {
  --color-primary: royalblue;
  --color-primary-dark: #1d4ed8;
  --color-text: #1e293b;
  --color-border: #e2e8f0;
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --radius: 12px;
}

/* 2. BASE */
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: Arial, Helvetica, sans-serif; line-height: 1.6; color: var(--color-text); }

/* 3. LAYOUT */
.page { max-width: 640px; margin: 0 auto; padding: var(--space-3); }

/* 4. COMPONENTS */

/* --- site-header --- */
.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  background-color: #0f172a;
}
.site-header__logo { color: white; font-weight: bold; text-decoration: none; }

/* --- nav --- */
.nav__list { display: flex; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
.nav__link { display: block; padding: var(--space-1) var(--space-2); color: #cbd5e1; text-decoration: none; }
.nav__link--active { color: white; background-color: var(--color-primary); border-radius: 999px; }

/* --- card --- */
.card { padding: var(--space-3); margin-bottom: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius); }
.card__title { margin: 0 0 var(--space-1); }
.card__text { margin: 0 0 var(--space-2); color: #475569; }
.card__actions { display: flex; gap: var(--space-1); }
.card--featured { border-color: var(--color-primary); background-color: #eff6ff; }

/* --- btn --- */
.btn { display: inline-block; padding: 0.6rem 1.1rem; border: 2px solid transparent; border-radius: 8px; font: inherit; text-decoration: none; cursor: pointer; }
.btn--primary { background-color: var(--color-primary); color: white; }
.btn--primary:hover { background-color: var(--color-primary-dark); }
.btn--ghost { background-color: transparent; color: var(--color-primary); border-color: var(--color-primary); }
.btn--small { padding: 0.35rem 0.8rem; font-size: 0.875rem; }

Common mistakes

  • Inconsistent naming: card-title in one place, cardTitle in another, card__title in a third. Pick one.
  • Chaining elements: .card__actions__button. Keep names flat: .card__button.
  • Using a modifier without its base class, such as class="btn--primary" alone.
  • Descendant selectors in components (.card h2), which tie the CSS to the HTML structure.
  • Naming by appearance (.red-box, .left-column).
  • Styling ids or fighting specificity with !important.
  • One giant “misc” section where all the leftovers go.
  • Duplicating the same block in several files.
  • Over-engineering a tiny site with a dozen files. Match the structure to the size of the project.
  • Never deleting anything. Unused CSS is a cost for every visitor.

Practice

  1. Click Practice in Editor. Add the .nav__list, .nav__item and .nav__link rules so the navigation sits in a row. Then style .nav__link--active.

  2. Write the .card block and its three elements, then add the .card--featured modifier. Does the second card change without any extra HTML?

  3. Build the .btn block with --primary and --ghost. Add a third modifier, --danger, and use it on a new button.

  4. Combine btn btn--primary btn--small on one button. Which rules apply?

  5. Rename .site-header__logo to what it would be if it were its own block. Is a logo a “part of the header” or a reusable component?

  6. Take this messy HTML and rewrite it with BEM names, then write its CSS with flat selectors:

    <div class="box red">
      <div class="top"><h3>Title</h3></div>
      <div class="bottom"><a class="link big">Open</a></div>
    </div>
  7. Replace .nav__link--active with an [aria-current="page"] selector and add the attribute to the HTML.

  8. Reorganise your stylesheet using the five-section order and a table of contents comment at the top.

Recap

  • Large stylesheets fail because of name collisions, specificity wars, coupling to HTML, dead code, and disorder.
  • BEM names things as Block (card), Element (card__title) and Modifier (card--featured). Every rule is one flat class selector (specificity 0-1-0).
  • A modifier always goes next to the base class. Elements stay flat (no a__b__c). Name by purpose, not by looks.
  • Prefer ARIA attributes (aria-current, aria-expanded) for state styling, and is- / has- classes otherwise.
  • Keep selectors class-based, avoid ids, !important and tag selectors in components, and use variables for repeated values.
  • Organise the CSS from general to specific: settings, base, layout, components, utilities. Mirror it with @layer and, when the file grows, with one file per component.
  • Combine files for production because @import chains load slowly. That is part of the next lesson on performance and accessibility.