Vivian Voss

READS: The Five Prefixes

css architecture web

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:

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

READS: The Five Prefixes semantic → structure → variant → feature → state 1 as-* Structure as-grid as-stack as-sidebar as-btn as-card 2 like-* Variant like-primary like-ghost like-large like-compact 3 show-* Feature toggle show-icon show-badge show-avatar show-close 4 has-* Capability has-shadow has-border has-divider has-scroll 5 is-* State is-active is-hidden is-disabled is-loading Order matters. State last: it changes at runtime.

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.

The Same Button BEM encodes location card__footer-btn card__footer-btn--primary card__footer-btn--large card__footer-btn--disabled card__footer-btn--icon 5 classes, 120 characters Specificity per selector: 0,1,0 READS encodes meaning footer-cta semantic as-btn structure like-large like-primary variant show-icon feature is-disabled state 6 classes, 62 characters Specificity per selector: 0,1,0 Same specificity. Same result. Half the characters. One encodes location. One encodes meaning.

Order as Grammar

The sequence of classes is not aesthetic preference. It is grammar. The reading order follows a deliberate hierarchy:

  1. Semantic identity (footer-cta) answers “What is this element's unique role on the page?”
  2. Structure (as-btn) answers “What structural pattern does it follow?”
  3. Variant (like-large, like-primary) answers “How does this instance differ from the default?”
  4. Feature (show-icon) answers “What optional features are enabled?”
  5. 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.