Stack Patterns ■ Episode 02
“Read it, understand it. No derivation required.”
In 2009, Nicole Sullivan presented Object-Oriented CSS at a Yahoo engineering talk. The premise was startlingly simple: separate structure from skin, separate container from content. Write classes that describe what a thing is, not where it lives. The DOM becomes a composition of small, reusable objects rather than a tree of deeply nested, location-dependent rules.
One year later, in 2010, Yandex published BEM: Block, Element, Modifier. Rather more aggressive marketing. Conference talks. Blog posts. Official documentation with diagrams. The message was clear: specificity wars are over, because we have invented underscores.
Guess which one won the popularity contest.
Not the clever one. The loud one. Marvellous.
The Specificity Myth
BEM's central claim was that it “prevents specificity
wars.” This is worth examining, because it is the
foundational argument that justified fifteen years of
card__footer-btn--primary chains polluting every
DOM in the industry.
Consider the specificity of a BEM selector:
.card__footer-btn--primary { color: coral; }
/* Specificity: 0,1,0 */
Now consider the specificity of an OOCSS selector:
.like-primary { color: coral; }
/* Specificity: 0,1,0 */
Identical. (0,1,0) in both cases. A single class
selector is a single class selector, regardless of how many
hyphens and underscores one embeds within it. BEM does not
lower specificity. It does not raise specificity. It produces
precisely the same specificity as any other single-class
methodology, but with considerably longer strings.
The “specificity prevention” was never a technical mechanism. It was a social contract: “If everyone uses only single classes, no one escalates.” Which is true, and also true of OOCSS, SMACSS, Atomic CSS, and simply agreeing not to use IDs for styling. The contract is the solution. The naming convention is decoration.
In 2026, of course, the actual solution to specificity conflicts
is
@layer.
Layer order outranks specificity entirely. But that is the subject of
another episode.
What Sullivan Actually Said
OOCSS introduced two principles that remain as sound today as they were in 2009:
- Separate structure from skin. Layout (grid, flex, spacing) belongs in one set of classes. Visual appearance (colour, shadow, border-radius) belongs in another. A card's structure does not change when the theme does.
- Separate container from content. A heading inside a sidebar should not be styled differently merely because it lives inside a sidebar. The heading's classes describe what it is, not where it is.
These are architectural principles. They say nothing about underscores, double hyphens, or encoding the entire DOM tree into a class name. Sullivan was describing how to think about CSS objects. BEM was describing how to spell them.
The Five Prefixes
READS (REadable Attribute Description Syntax) is an OOCSS dialect that takes Sullivan's principles and gives them a vocabulary. Five prefixes. One meaning each. No ambiguity.
as-* declares structure. What
is this element? A grid, a stack, a sidebar, a button,
a card. The structural identity. In Sullivan's terms, this is
the “object” itself.
like-* declares variant. How does
it differ from the default? Primary, ghost, large,
compact. This is the skin, separated from structure exactly as
OOCSS prescribes.
show-* is a feature toggle. The
component can display an icon, a badge, a close button.
Whether it does is a matter of composition, not a separate
component class.
has-* declares a capability. The
element possesses a shadow, a border, a divider. These are
visual attributes that apply independently of structure or
variant.
is-* declares state. Active,
hidden, disabled, loading. State comes last because it changes
at runtime. Everything before it is static. The order is not
arbitrary; it reflects the lifecycle of the element.
One semantic class is permitted for identity:
footer-cta, main-nav,
hero-title. This is the element's proper name,
unique within the page. IDs are reserved for CSS anchors,
fragment targets, and JavaScript hooks.
The Same Button, Two Philosophies
The difference becomes visceral when you see it in the DOM. Here is the same button, described once in BEM and once in READS:
<!-- BEM -->
<button class="card__footer-btn card__footer-btn--primary
card__footer-btn--large card__footer-btn--disabled
card__footer-btn--icon">
Submit
</button>
<!-- READS -->
<button class="footer-cta as-btn like-large like-primary
show-icon is-disabled">
Submit
</button>
The BEM version encodes location. This button is the
footer button of the card. It is primary, large, disabled, and
has an icon. To understand what
card__footer-btn--primary means, you must first
parse the block (card), then the element
(footer-btn), then the modifier
(primary). Three levels of derivation. Every
class repeats the full ancestry.
The READS version encodes meaning. Read it aloud:
“The footer-cta is a button, like large and primary, that shows an icon and is disabled.”
It reads like a sentence. No derivation required. A developer seeing this markup for the first time understands the element's role, its visual treatment, its features, and its state. The prefix tells you the category of the class before you even read its name.
Order as Grammar
The sequence of classes is not aesthetic preference. It is grammar. The reading order follows a deliberate hierarchy:
- Semantic identity
(
footer-cta) answers “What is this element's unique role on the page?” - Structure (
as-btn) answers “What structural pattern does it follow?” - Variant (
like-large,like-primary) answers “How does this instance differ from the default?” - Feature (
show-icon) answers “What optional features are enabled?” - State (
is-disabled) answers “What is its current runtime condition?”
The progression moves from the most permanent to the most
volatile. Identity never changes. Structure rarely does.
Variants are set once per instance. Features may toggle. State
changes constantly. When JavaScript adds or removes a class at
runtime, it is always a state class. Always at the end.
Always is-*.
This is not a convention you must memorise. It is a convention you read. The prefix tells you where in the sentence you are.
The CSS Side
READS truly comes into its own when you write the stylesheet. Each prefix maps to a clear category of rules, and native CSS nesting makes the structure almost absurdly clean:
.as-card {
padding: var(--space-m);
border-radius: var(--radius-m);
background: var(--surface);
&.like-primary { background: var(--colour-primary); }
&.like-ghost { background: transparent; border: 1px solid var(--border); }
&.show-badge::after { content: attr(data-count); /* badge */ }
&.has-shadow { box-shadow: var(--shadow-m); }
&.has-border { border: 1px solid var(--border); }
&.is-active { outline: 2px solid var(--colour-focus); }
&.is-hidden { display: none; }
}
Every rule nests under the structural class. Every nested rule
uses a compound selector (&.like-primary),
which produces specificity (0,2,0): still flat,
still predictable, still trivially overridable in a higher
@layer. The prefix categorises the rule visually
in the stylesheet just as it categorises the class in the
markup.
Compare this with the BEM equivalent:
.card { padding: 1rem; }
.card--primary { background: coral; }
.card--ghost { background: transparent; }
.card--shadow { box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.card--active { outline: 2px solid blue; }
Every modifier is a flat sibling. There is no visual hierarchy in the stylesheet. “Primary” sits next to “shadow” sits next to “active.” Are they variants? Features? States? The naming convention does not distinguish. The developer must remember. READS prefixes make remembering unnecessary.
BEM's Real Contribution
It would be uncharitable to dismiss BEM entirely. Its genuine
contribution was popularising the idea that CSS classes should be
flat (single-class selectors, no nesting, no IDs for styling).
Before BEM, it was entirely normal to encounter selectors like
#main .sidebar ul li a.active with a specificity
of (1,2,3). BEM's “only use classes”
convention was a meaningful improvement.
The error was in conflating the convention (use flat selectors) with the syntax (encode the DOM tree into the class name). The convention was sound. The syntax was unnecessary. OOCSS had the same convention a year earlier, without the syntactic overhead.
And in 2026, neither convention solves specificity conflicts on
its own. That is what
@layer
does. Naming conventions handle readability.
@layer handles precedence. Confusing the
two is how the industry spent fifteen years solving the wrong
problem with the wrong tool.
The Compound Noun Problem
There is a linguistic observation that rarely gets made about BEM, though it explains much of the friction developers feel without being able to articulate it. BEM class names are compound nouns. German-style compound nouns, in fact: the meaning is built by agglutination, and the reader must decompose the compound mentally before comprehension arrives.
card__footer-btn--primary--large--disabled is the
CSS equivalent of
Kartenfussbereichsschaltflaechenhauptgrossdeaktiviert.
Perfectly logical. Entirely parseable. And no one would call it
readable.
READS classes are adjectives. They describe. They modify.
They compose. as-btn like-large like-primary is-disabled
is English prose: subject, description, state. The cognitive
cost is not zero, but it is dramatically lower, because the
reader processes each class independently rather than
decomposing a single monolithic string.
When READS Meets @layer
The combination of READS prefixes with CSS Cascade Layers produces an architecture that is both readable and conflict-free. Structure, variant, feature, and state can each live in their own layer if the project demands it:
@layer base, structure, variants, features, states, themes;
@layer structure {
.as-card { display: grid; padding: var(--space-m); }
.as-stack { display: flex; flex-direction: column; gap: var(--space-s); }
.as-btn { display: inline-flex; padding: var(--space-xs) var(--space-m); }
}
@layer variants {
.like-primary { background: var(--colour-primary); color: var(--colour-on-primary); }
.like-ghost { background: transparent; }
.like-large { font-size: var(--text-lg); padding: var(--space-s) var(--space-l); }
}
@layer states {
.is-active { outline: 2px solid var(--colour-focus); }
.is-disabled { opacity: 0.5; pointer-events: none; }
.is-hidden { display: none; }
}
State always wins over variant. Variant always wins over
structure. Not because of
specificity arithmetic,
but because layer order dictates precedence. The naming
convention and the cascade architecture reinforce each other.
READS tells you what a class means. @layer
tells the browser what wins.
The Acronym
READS: REadable Attribute Description Syntax.
It is not a framework. There is no npm package. There is no CLI. There is no build step. It is a naming convention: five prefixes, one semantic class, a defined order. The entire specification fits in a single paragraph. The entire philosophy fits in a single sentence: read it, understand it, no derivation required.
Nicole Sullivan described the architecture in 2009. READS simply gives it a spelling that the next developer can parse without a README.