Skip to content
v2026.1

CSS architecture for
OCD-grade determinism.

Collision-proof namespaces. One CSS file per component. State in data-* attributes. Locked-down semantic tokens. No required preprocessor, no runtime, and no div wearing fourteen classes.

BBlock.user_cardIdentity, in snake_case.
EElement.user_card-titleA part, joined by one hyphen.
AAttribute[data-state]State, as data.
MModuleUserCard.cssOne block, one file.
01The problem

CSS is more capable than ever, yet more confusing than ever.

Devographics asked 4,902 developers what hurts. Here's the freshest data:

Top pain points – color573 respondents
  • Accessibility27%Contrast, by hand, forever.
  • Theming24%Named cause: duplicated theme definitions.
  • Cognitive overload17%
  • Custom properties7%The tool meant to fix the above.
Top pain points – CSS in general398 respondents
  • Cognitive overload22%The number one complaint, overall.
  • Tooling13%
  • Maintenance9%Up 18 places from last year.
  • Cascade management5%

“Supporting light/dark mode is difficult; developers struggle to work with system preferences, manual theme selection, and duplicated theme definitions.”

“Managing color palettes, tokens, variables and design-system scales is hard.”

Nobody is waiting on a browser anymore. The named causes are duplicated theme definitions, palettes that are hard to manage, cognitive overload, and maintenance.

That is an architecture problem. That's what we solve.

02Provenance

BEAM did not fall out of the sky.

Almost every idea here is borrowed. A convention nobody has seen before is a convention nobody will follow.

  1. 2008OOCSS

    Separate structure from skin.

    Became Mass vs Void: object shape and page rhythm are different budgets.

  2. 2009BEM

    Namespace every component.

    Kept the idea, dropped the typing tax of __ and --.

  3. 2011SMACSS

    Categorize rules before you write them.

    Became the prefix taxonomy: l_, u_, g_, and the block itself.

  4. 2014CSS-in-JS

    Scoping is not optional.

    Kept co-location, refused the runtime and the generated class names.

  5. 2014ITCSS

    Order the cascade deliberately.

    Handed the job to :where() and a flat file layout instead.

  6. 2017Atomic / utility-first

    Constraints beat freedom.

    Kept the constrained scale, put it in tokens instead of the class attribute.

  7. 2020CUBE CSS

    Compose layout, do not enumerate it.

    Became the l_* primitives and the Binary Rule.

03The tetrad

Four letters. That's the entire principle.

A rule you have to look up is a rule you will break.

Block

.snake_case

Named for what it is. .user_card, not .bg-white.rounded-xl.p-4. When the fill changes, bg-white is a lie in forty files. A card is still a card.

snake_case because user_card is one double-click. id is a document hook, not a styling API – specificity 1,0,0, not reusable.

UserCard.cssCSS
.user_card {
  padding: var(--space-5);
  border: 1px solid var(--border-base);
  background: var(--bg-surface);
}

Element

.block-part_name

One hyphen joins a block to its part. Underscores inside each half. The hyphen means belongs to, and nothing else. Names stay flat – they do not encode today's DOM.

NavBar.cssCSS
/* Correct: flat, one hyphen, searchable */
.nav_bar-list_item { }
.nav_bar-action_button { }

/* Wrong: the hyphen now means two different things */
.nav_bar-list-item { }

/* Wrong: encodes today's DOM into tomorrow's selector */
.nav_bar .list_item { }

Attribute

[data-state]

State is data, not a class. is-active--active.button--loading.js-open – five codebases, five dialects. Attributes have been around since 2008, and they are the best tool for the job. You also stop concatenating class strings in JavaScript.

Button.htmlHTML
<button class="button" data-variant="primary" data-state="loading">
  Save
</button>

Module

UserCard.css

One block, one file, next to the component. UserCard.tsx and UserCard.css live and die together. No orphans. No four-thousand-line components.css. The exceptions are big components, implementing full pages, like index.astro 

treeText
src/components/
  UserCard.tsx     -> renders .user_card
  UserCard.css     -> styles .user_card and nothing else
  PromoBanner.tsx
  PromoBanner.css
04Editor optimization

The convention is designed around your cursor.

To make it work – never use the ampersand to build a class name.

Wrong – the name does not existCSS
.user_card {
  &-title {
    font-size: var(--text-2xl);
  }
}
Right – the name is a real stringCSS
.user_card-title {
  font-size: var(--text-2xl);
}

In the first version, user_card-title exists nowhere – not in search, not in rg, not in the language server, not in a model's context window. You saved eleven characters.

In the second, one string is the markup and the style. D or * cgn . can grab it because it is actually in the file. &-title gives those keystrokes nothing. BEM's &__element has the same hole.

05The 24 percent

Theming, solved.

A quarter of developers named theming as a top color pain point, and the survey recorded the specific cause: duplicated theme definitions. So let's remove the duplication instead of managing it.

theme.css – Layers 1 and 2CSS
/* 1. Foundations. Raw materials. Never used in a component. */
:root {
  --palette-white: oklch(1 0 0);
  --palette-stone-900: oklch(21.6% 0.006 56.043);
}

/* 2. Themes. What "light" and "dark" physically mean.
      Components must never touch these either. */
:root {
  --theme-light-bg-surface: var(--palette-white);
  --theme-dark-bg-surface: var(--palette-stone-900);
}
theme.css – Layer 3, the public contractCSS
/* 3. Semantics. The only layer a component may read. */
[data-theme='light'],
[data-theme='dark'] [data-theme='inverse'] {
  --bg-surface: var(--theme-light-bg-surface);
}

[data-theme='dark'],
[data-theme='light'] [data-theme='inverse'] {
  --bg-surface: var(--theme-dark-bg-surface);
}

A semantic is declared twice: light block, dark block. Components read var(--bg-surface) and stop.

A third theme is one more block. Inverse is one attribute on a subtree. No component has ever heard the word “dark.”

Refraction

Same markup. Same stylesheet. Not one conditional color in the component.

PrimaryNeutral
data-theme="light"

Refraction

Same markup. Same stylesheet. Not one conditional color in the component.

PrimaryNeutral
data-theme="dark"

Refraction

Same markup. Same stylesheet. Not one conditional color in the component.

PrimaryNeutral
data-theme="inverse"

Flip the theme in the header. The first two columns hold their ground; the third flips with you, because inverse resolves against whatever context it lands in.

06Space

Void and mass are different beasts.

padding: 1rem and width: 1rem are not the same kind of 1rem.

Void is marginpaddinggap. Shared rhythm. Always --space-*.

Mass is widthheightinset,  translate(). The avatar is 3rem because the avatar is 3rem, not because --space-12 happens to equal that. If you tighten the scale – the avatar should not become an oval.

Avatar.cssCSS
.profile_card {
  /* Void: shared rhythm, always a token */
  padding: var(--space-5);
  gap: var(--space-3);
}

.profile_card-avatar {
  /* Mass: this object's own shape */
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
}
Wrong – the card relies on the generic layoutHTML
<article class="l_stack user_card" data-gap="4">
  ...
</article>
Right – the card owns its own layout in CSSHTML
<article class="user_card">
  ...
</article>
07Interpolation

No bloated fluid formula.

The State of CSS missing-features list asked for “simpler fluid typography primitives.” It turns out to be about forty lines of PostCSS.

What we all copy-pasteCSS
font-size: clamp(
  2.25rem,
  calc(2.25rem + (8 - 2.25) * ((100vw - 40rem) / (80 - 40))),
  8rem
);
What you actually meantCSS
font-size: fluid(var(--text-4xl), var(--text-9xl));

Nobody audits the first one. The two 2.25rems drift. fluid() , on the other hand, resolves both tokens at build and emits a clamp() with the slope already computed.

Refraction

768px viewport1.5rem clamped at minimum
You write
fluid(var(--text-2xl), var(--text-6xl))
You ship
clamp(1.5rem, -0.75rem + 5.625vw, 3.75rem)
08Rosetta stone

One card, four dialects.

Same component, same design, four philosophies. Read each one and ask the only question that matters: what happens to this in eighteen months, when the person working on it has never met you?

Right instinct, four extra characters per name. Nest &__title and card__title exists nowhere in the repo.

markup.htmlHTML
<article class="promo-card promo-card--featured">
  <h2 class="promo-card__title">Refraction</h2>
  <button class="promo-card__action promo-card__action--loading">
    Read
  </button>
</article>
promo-card.scssCSS
.promo-card {
  &__title { font-size: 1.5rem; }

  &__action {
    &--loading { opacity: 0.5; }
  }

  &--featured { border-color: #e11d48; }
}
09Machines

CSS is the language AI is worst at. That is not a coincidence.

The 2026 survey puts AI-generated CSS at roughly 28 percent – the lowest share anywhere in the stack. Here is my theory.

There is no ground truth to generate against. A model writing Go has a compiler. Writing SQL, a schema. Writing TypeScript, types. Writing CSS, it has vibes.

Ask five senior engineers to name and structure a card component and you will get five answers, all defensible. A model trained on all five produces a sixth. Then it produces a seventh in the next file, because nothing told it what the sixth was.

BEAM's rules are the kind machines are actually good at, because they are mechanical, strict and deterministic rather than tasteful.

What the agent is toldText
Block            snake_case, from the filename
Element          .block-part_name, always flat
Attribute        data-*, never a state class
Module           one block, one co-located file
Color            Layer 3 semantics only
Void             --space-* for margin/padding/gap
Mass             raw rem for width/height/inset
Layout           l_* on a wrapper, never the block
Selectors        classes only, never an ID
Queries          min-width only, never max-width
Nesting          one level, conditions only
Ampersand        banned for name building
10Full disclosure

What you are going to hate about it.

Every architecture doc ends with a triumphant list of benefits. Here is the other list, because you are going to find it anyway and I would rather you heard it from me.

“It's more typing than utilities.”

Correct. A block plus a CSS file is slower than fourteen utilities – the first time. It is faster the third time you touch that component, and dramatically faster the first time you rename it. If your component is genuinely one-off, utilities win. Most components are not one-off.

“snake_case looks strange.”

For about a day. Then double-clicking a class name and getting the whole class name stops feeling like a trick and starts feeling like the baseline. Nobody has ever asked to go back.

“I want mixins.”

So does everyone – mixins have been in the survey's top missing features for years, and the platform is finally building them. Until then, custom properties cover most of it and honest duplication covers the rest. That is a real cost, and it is smaller than a preprocessor.

“It's rigid to the point of rude.”

That part is load-bearing. A convention with exceptions is a convention that gets quietly abandoned in month three. The rules are absolute so that nobody has to relitigate them in a pull request at 6pm.

“Will it stop me writing bad CSS?”

No. Nothing will. What it does is make bad CSS visible – a raw hex code, a state class, an l_ class riding a block. You stop arguing about taste and start pointing at rules.

“Is this just BEM with extra steps?”

It is BEM with fewer steps, plus the two things BEM never had: state that lives in attributes, and a theme engine that stops color from being copy-pasted. If you already run disciplined BEM, migration is mostly a find-and-replace.

Begin

Name things for what they are. Everything else follows.

Six stylesheets, one PostCSS plugin, and a set of rules short enough to memorize. You can convert one component this afternoon and keep the rest of your codebase exactly as it is.

Then open the inspector on this page. Every class you find is a Block, an Element, an l_, a u_, or a g_. That is the whole vocabulary.