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.
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
- 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.
- 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 -->
- Elements belong to the block, not to each other. Do not chain:
.card__actions__buttonis wrong. Name it for the block it belongs to:.card__button. Elements can be nested deeply in the HTML, but their names stay flat. - Name by purpose, not appearance.
.card__titlestays correct when the title turns green..big-red-textbecomes a lie the day the design changes. - Different components get different blocks. A button inside a card is not a “card element”. It is a separate
btnblock that the card happens to contain. That is why the card usesclass="btn btn--primary"and notcard__button. - 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 h2couples CSS to HTML). - Keep specificity flat. Aim for 0-1-0 almost everywhere. Use
:where()for resets. - Do not use
!importantexcept in a utility class. - Never style by position in the document (
div:nth-child(3) > span). - One component, one place. All the rules of
.cardlive 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:
- Settings:
:rootvariables. - Base: the reset and defaults for plain elements (
body,a,h1). - Layout: the page structure (
.page,.container, grids). - Components: each block (
.card,.btn,.nav), one section per block. - 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-titlein one place,cardTitlein another,card__titlein 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
-
Click Practice in Editor. Add the
.nav__list,.nav__itemand.nav__linkrules so the navigation sits in a row. Then style.nav__link--active. -
Write the
.cardblock and its three elements, then add the.card--featuredmodifier. Does the second card change without any extra HTML? -
Build the
.btnblock with--primaryand--ghost. Add a third modifier,--danger, and use it on a new button. -
Combine
btn btn--primary btn--smallon one button. Which rules apply? -
Rename
.site-header__logoto what it would be if it were its own block. Is a logo a “part of the header” or a reusable component? -
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> -
Replace
.nav__link--activewith an[aria-current="page"]selector and add the attribute to the HTML. -
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, andis-/has-classes otherwise. - Keep selectors class-based, avoid ids,
!importantand 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
@layerand, when the file grows, with one file per component. - Combine files for production because
@importchains load slowly. That is part of the next lesson on performance and accessibility.