
CSS Architecture Compared: BEM, OOCSS, SMACSS, and ITCSS
Every stylesheet starts clean. A few hundred lines in, it's still manageable. A few thousand lines and three developers later, you're adding !important to beat a selector you can't find, and nobody dares delete anything because it might be used somewhere. CSS architecture is the set of decisions that stops that decay: how you name things, how you split files, how you order rules, and how you keep specificity under control.
Four methodologies have shaped how most of us think about this: OOCSS, SMACSS, BEM, and ITCSS. They're often presented as competitors, but they actually answer different questions, and most mature codebases borrow from several. Let's look at each, compare them side by side, and then put together a practical hybrid.
The Problems Every Architecture Tries to Solve
Before comparing solutions, it helps to name the problems:
- Global scope. Every class can affect every element on every page.
- Specificity wars. Selectors get longer and stronger to override each other.
- Source-order dependence. Moving a file import changes what wins.
- Dead code. Nobody knows what's safe to remove.
- Duplication. The same padding, colors, and shadows get redeclared everywhere.
- Onboarding. New developers can't tell where to put new CSS.
Each methodology below attacks a different subset of these.
OOCSS: Object-Oriented CSS
OOCSS was introduced by Nicole Sullivan around 2008, and it was one of the first serious attempts to bring software design principles to CSS. It's built on two ideas.
1. Separate Structure from Skin
Structure is layout and dimensions. Skin is colors, borders, shadows, and fonts. Keep them in separate classes so each can be reused independently.
/* Structure */
.box {
padding: 1.5rem;
border-radius: 8px;
}
/* Skins */
.skin-light {
background: #ffffff;
border: 1px solid #e2e8f0;
}
.skin-dark {
background: #0f172a;
color: #e2e8f0;
}
<div class="box skin-light">...</div>
<div class="box skin-dark">...</div>
2. Separate Container from Content
An object should look the same wherever you put it. Don't style it based on its location:
/* Location-dependent: avoid */
.sidebar h3 {
font-size: 1.125rem;
}
/* Location-independent: prefer */
.heading-small {
font-size: 1.125rem;
}
The Media Object
OOCSS's most famous contribution is the media object, an image beside some text, which turns out to be everywhere: comments, user lists, notifications, and product rows. Written once, it's reused across the whole site:
.media {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.media__figure {
flex-shrink: 0;
}
.media__body {
flex: 1;
min-width: 0;
}
Strengths: reuse, smaller stylesheets, and a mindset of spotting repeated patterns. Weaknesses: it's a set of principles, not a naming or file system, and taken to extremes it produces HTML full of presentational classes. It's also a direct ancestor of utility-first CSS.
SMACSS: Scalable and Modular Architecture for CSS
SMACSS (pronounced "smacks"), by Jonathan Snook, focuses on categorizing rules. Snook proposed five categories:
- Base: element defaults with no classes:
html,body,a,h1, form elements. - Layout: major page regions. Snook suggested prefixing with
l-:.l-header,.l-sidebar,.l-grid. - Module: the reusable components that make up most of the CSS:
.card,.nav,.callout. - State: how things look in a particular state, prefixed with
is-:.is-active,.is-hidden,.is-collapsed. - Theme: optional overrides for different looks or brands.
/* Base */
a {
color: #1d4ed8;
}
/* Layout */
.l-sidebar {
width: 18rem;
}
/* Module */
.callout {
padding: 1rem;
border-left: 4px solid #0ea5e9;
}
.callout-title {
font-weight: 700;
}
/* State */
.is-collapsed {
display: none;
}
SMACSS's is- state prefix is so useful that it's been widely adopted by teams that use none of the rest of SMACSS. When you see .is-open in a BEM codebase, that's SMACSS influence.
Strengths: a clear answer to "which file does this go in?" and a readable vocabulary for state. Weaknesses: module naming is less strict than BEM, so collisions can still happen, and it doesn't say much about ordering or specificity.
BEM: Block, Element, Modifier
BEM, from Yandex, is primarily a naming convention:
.block
.block__element
.block--modifier
<nav class="breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a class="breadcrumb__link" href="/">Home</a>
</li>
<li class="breadcrumb__item breadcrumb__item--current" aria-current="page">
Pricing
</li>
</ol>
</nav>
.breadcrumb__list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0;
list-style: none;
}
.breadcrumb__item:not(:last-child)::after {
content: "/";
margin-inline-start: 0.5rem;
color: #94a3b8;
}
.breadcrumb__item--current {
font-weight: 600;
}
Every selector is a single class, so specificity stays flat. Every class is namespaced by its block, so collisions essentially disappear. And the markup documents its own structure.
Strengths: strict, easy to lint, flat specificity, and excellent for component-based work.
Weaknesses: long class names, and it says nothing about file organization or global styles. It's also easy to misuse by chaining elements (.block__elem__elem).
ITCSS: Inverted Triangle CSS
ITCSS, created by Harry Roberts, is about ordering. It organizes CSS into layers that go from broad, low-specificity, far-reaching rules at the top to narrow, high-specificity, targeted rules at the bottom. Visualized, it's an upside-down triangle.
- Settings: variables and design tokens. No CSS output on its own in a preprocessor, or custom properties on
:root. - Tools: mixins and functions (preprocessor only).
- Generic: resets, normalize,
box-sizing. - Elements: bare HTML element styles, like
h1anda. - Objects: undecorated structural patterns, like
.o-container,.o-stack, and.o-media(the OOCSS layer). - Components: designed UI pieces:
.c-card,.c-button. - Utilities: single-purpose helpers that must always win:
.u-hidden,.u-text-center.
// main.scss
@use "settings/tokens";
@use "tools/mixins";
@use "generic/reset";
@use "elements/typography";
@use "objects/container";
@use "objects/stack";
@use "components/card";
@use "components/button";
@use "utilities/visibility";
Because each layer is more specific than the one before, later rules naturally override earlier ones without specificity battles. The o-, c-, and u- prefixes make a class's layer obvious in the HTML.
Strengths: solves source-order and specificity problems at the structural level, and scales to very large codebases. Weaknesses: it's a structure, not a naming convention, and it has more upfront ceremony than the others.
Side-by-Side Comparison
| OOCSS | SMACSS | BEM | ITCSS | |
|---|---|---|---|---|
| Main focus | Reuse | Categorization | Naming | Ordering |
| Naming rules | Loose | Prefixes (l-, is-) | Strict | Prefixes (o-, c-, u-) |
| File structure | None | 5 categories | None | 7 layers |
| Specificity control | Indirect | Partial | Strong (flat) | Strong (layered) |
| Learning curve | Low | Low | Low to medium | Medium |
| Best for | Spotting patterns | Mid-size sites | Component UIs | Large, long-lived codebases |
The table makes it clear why they're complementary. BEM tells you how to name a component. ITCSS tells you where that component sits in the cascade. OOCSS reminds you to extract repeated structure into objects. SMACSS gives you a vocabulary for states.
Cascade Layers: ITCSS, Built Into the Browser
Cascade layers (@layer), supported in all current major browsers, give you ITCSS-style ordering natively. Layers declared earlier lose to layers declared later, regardless of selector specificity:
@layer reset, base, objects, components, utilities;
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
}
@layer base {
a {
color: #1d4ed8;
}
}
@layer components {
.c-card a {
color: #0f766e;
}
}
@layer utilities {
.u-text-muted {
color: #64748b;
}
}
Now .u-text-muted beats .c-card a even though it's less specific, because the utilities layer is declared later. That's exactly what ITCSS was trying to achieve through careful ordering and naming, and it removes the temptation to use !important for utilities.
One important detail: unlayered styles beat all layered styles. That's useful for third-party CSS. Put it in an early layer so your own code wins easily:
@layer vendor, reset, base, components, utilities;
@import url("vendor/datepicker.css") layer(vendor);
Note that @import rules must come before all other rules except @charset and @layer statements, which is why the layer order statement can sit above the import.
Scoping: @scope and CSS Modules
The collision problem that BEM solves by convention can also be solved by tooling:
- CSS Modules rewrite class names at build time, so
.titleinCard.module.cssbecomes something like.Card_title_x7f2a. - Shadow DOM fully encapsulates styles inside web components.
- @scope limits selectors to a subtree natively. It's supported in Chromium-based browsers and Safari, and Firefox support has been arriving more recently, so check current support before relying on it without a fallback.
@scope (.card) to (.card__content) {
img {
border-radius: 8px;
}
}
These don't make methodologies obsolete. Even with CSS Modules, you still need to decide how to layer global styles, what goes into shared objects, and how to express state.
A Practical Hybrid
Here's the structure I reach for on most projects today:
styles/
settings/ tokens as custom properties
generic/ reset, box-sizing
elements/ typography, links, forms
objects/ .o-container, .o-stack, .o-cluster, .o-grid
components/ .c-card, .c-card__title, .c-card--featured
utilities/ .u-visually-hidden, .u-flow
main.css @layer order + imports
- ITCSS for the folders and layer order, enforced with
@layer. - OOCSS thinking for the objects layer: small, reusable layout primitives with no decoration.
- BEM naming inside components, with ITCSS prefixes.
- SMACSS-style
is-classes, or better, ARIA attributes, for state.
@layer settings, generic, elements, objects, components, utilities;
@layer objects {
.o-stack > * + * {
margin-block-start: var(--stack-space, 1rem);
}
}
@layer components {
.c-alert {
padding: 1rem 1.25rem;
border-inline-start: 4px solid var(--alert-color, #0ea5e9);
background: #f8fafc;
}
.c-alert--error {
--alert-color: #dc2626;
}
.c-alert__title {
margin: 0;
font-weight: 700;
}
.c-alert.is-dismissed {
display: none;
}
}
How to Choose
- Small site or a single developer: BEM naming alone is usually enough.
- Growing team: add ITCSS layers (via
@layer) so everyone knows where CSS goes. - Large design system: use all of it, and enforce naming with Stylelint.
- Framework with scoped styles (Vue SFCs, CSS Modules, Svelte): you can relax naming strictness, but still layer your global CSS.
- Utility-first framework: you're already using an extreme form of OOCSS. Architecture still matters for the custom components you write.
The most important thing is that the team agrees and the rules are written down. A mediocre methodology applied consistently beats a perfect one applied halfway.
Conclusion
OOCSS, SMACSS, BEM, and ITCSS each address a different part of the same problem. OOCSS is about reuse, SMACSS about categories, BEM about names, and ITCSS about order. Modern CSS has absorbed their best ideas: cascade layers give us ITCSS's ordering natively, and scoping tools reduce the need for naming discipline. But the underlying thinking still applies.
Pick the pieces that solve the problems your project actually has, document them, and let the linter do the policing. Your future self, and everyone who inherits your stylesheets, will thank you.


