Skip to content
v2026.1 · Block. Element. Attribute. Module.

You don't have a CSS problem. You have a naming problem.

BEAM is a strict, browser-native CSS architecture. Four kinds of class. One file per component. State indata-* attributes. Colour behind a firewall. No preprocessor, no runtime, and nodiv wearing fourteen classes.

One dependency. That's the tooling.
pnpm add -D postcss-beam-fluid
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 evidence

Every year we declare CSS solved. Every year the survey disagrees.

Devographics asked 4,902 developers what hurts. Read the answers slowly, because there is a pattern in them.

Top pain points — colour573 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 colour palettes, tokens, variables and design-system scales is hard.”

State of CSS 2026 — colour pain points, summarised from free-form answers

Now read those complaints again and notice what is missing. Nobody said color-mix() is broken. Nobody said custom properties don't work. Nobody is waiting on a browser.

Duplicated theme definitions. Palettes and scales that are “hard to manage.” Cognitive overload as the single most-mentioned pain point in the entire survey. Maintenance climbing eighteen places in one year.

None of that is a language problem. All of it is an architecture problem — the part of this you actually control, and the part almost nobody has written down.

So here it is, written down.

02Provenance

BEAM did not fall out of the sky.

Almost every idea in here is borrowed, and that is the strongest thing about it. 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

    Categorise 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 is the entire convention.

You can hold all of BEAM in your head. That is a design goal, not an accident — a rule you have to look up is a rule you will break.

Block

.snake_case

A component is named for what it is, never for what it looks like. .user_card, not.bg-white.rounded-xl.p-4.shadow-sm.

That reads like taste until the design changes. The moment “white” becomes “white in light mode and stone-900 in dark mode,” a class called bg-white is a lie — and it is a lie in forty files at once..user_card was never going to be wrong, because a card is still a card.

Why snake_case? Because user_card is one double-click anduser-card is two. You will select this token thousands of times over the life of the project. Optimise for the thousands.

UserCard.cssCSS
/* The name survives every redesign. */
.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 separate words inside each half. So the hyphen means exactly one thing, everywhere, forever: belongs to.

And element names stay flat. They never mirror how deeply the markup happens to nest, because markup nests differently next quarter and a class name that encoded the old shape is now actively misleading.

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]

This is the rule people argue about, and it is the one that pays off fastest. State is not a class. State is data.

Count the dialects you have shipped: is-active, --active,.button--loading, .open, .js-open. Five codebases, five conventions, and not one of them can be queried, typed, or read at a glance in devtools. Meanwhile the platform has had a first-class answer since 2008, and aria-* has been using it the whole time.

You also stop concatenating class strings in JavaScript, which is where roughly half of all UI state bugs come from.

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

Module

UserCard.css

One block, one file, sitting next to the component that renders it.UserCard.tsx and UserCard.css live and die together.

Delete the component, delete the stylesheet. No orphan rules. No “is anything still using this?” No four-thousand-line components.css that everyone appends to and nobody removes from. The file tree becomes an index of your UI.

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

The convention is designed around your cursor.

Here is a rule that sounds petty for about ten seconds: 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, the string user_card-title exists nowhere. Not in the stylesheet, not in your editor's search, not inrg, not in your language server's index, not in the context window of whatever model you asked to rename it. You created a class that is invisible to every tool you own, and you saved eleven characters.

In the second version, one search finds the markup and the style, and every tool that operates on strings suddenly works. In VS Code or Cursor, D keeps selecting occurrences until you have all of them. In Neovim, * puts the name in the search register, cgnchanges the next match, and . repeats that for the rest of the file. Rename it once, correctly, everywhere.

Notice that the Vim flow is not a shortcut for the same idea — it is the idea. *searches for the word under the cursor, so it cannot search for a name the file does not contain. Write&-title and the keystroke has nothing to grab. The convention is what makes the tooling possible, in either editor.

BEM understood half of this and then made the names longer..block__element--modifier costs four extra characters per name and a mental parse every time you read one — and &__element has exactly the same invisibility problem.

05The 24 percent

Theming, solved. Not eased — solved.

A quarter of developers named theming as a top colour 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);
}

That is the whole engine. A semantic token is declared exactly twice in the codebase: once in the light block, once in the dark block. Your components declare it zero times — they readvar(--bg-surface) and stop thinking.

Adding a third theme is one more block. A high-contrast mode is one more block. A per-tenant brand is one more block. There is never any per-component work, because no component has ever heard the word “dark.”

And because the semantic layer is just a pointer, any subtree can be told to resolve against the opposite theme. One attribute, no extra CSS anywhere:

Refraction

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

PrimaryNeutral
data-theme="light"

Refraction

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

PrimaryNeutral
data-theme="dark"

Refraction

Same markup. Same stylesheet. Not one conditional colour 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, becauseinverse resolves against whatever context it lands in.

06Space

Space is two different things and you have been treating it as one.

padding: 1rem and width: 1rem look like the same kind of number. They are not, and conflating them is why design systems drift.

Void is negative space — margin, padding, gap. It is rhythm, it is shared across the entire product, and it must come from the--space-* scale.

Mass is the physical shape of one object — width, height,inset, translate distances. The avatar is3rem because the avatar is 3rem. It is not --space-12; that is a numerical coincidence, and coincidences drift.

When somebody tightens the vertical rhythm by nudging --space-12 from 3rem to 2.75rem, your avatar should not quietly 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 knows where it livesHTML
<article class="l_stack user_card" data-gap="4">
  ...
</article>
Right — geometry is somebody else's jobHTML
<div class="l_stack" data-gap="4">
  <article class="user_card">...</article>
</div>
07Interpolation

The formula nobody reads, deleted.

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 reads the first one. Nobody audits it. Nobody notices when the second2.25rem drifts out of sync with the first, so it gets copied once and then quietly stops matching the type scale it was derived from.

fluid() resolves both tokens at build time and emits a plainclamp() — two bounds and a pre-computed slope. No multiplication, no division, no runtime, nothing left for the browser to do. Drag the viewport below and watch it work.

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 maintaining it has never met you?

The right instinct, taxed. Every name costs four extra characters, and &__title nesting means the string card__title exists nowhere in your 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. Everybody has a theory. Mine is boring.

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 rather than tasteful:

  • The class name is derivable from the filename. UserCard.tsx becomes.user_card. No judgement call.
  • The file location is derivable from the block. One block, one file, co-located. No judgement call.
  • Colour has exactly one legal source. If it is not a Layer 3 token, it is wrong.
  • State has exactly one legal form. If it is a class, it is wrong.
  • Layout has exactly one legal home. If it shares an element with a block, it is wrong.

Notice what those last three have in common: they are checkable. An agent can review its own diff before you ever see it, and you can review by pattern instead of by taste. That is the difference between a style guide and a specification.

It runs the other way too. Flat, searchable selectors mean an agent asked to touch.user_card-title can find every occurrence on the first try. A codebase built from&-title fragments is, to a model reading it, a maze with the labels torn off.

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
Colour           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
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, anl_ 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 colour 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.

Five stylesheets, an optional sixth for your fonts, one PostCSS plugin, and a set of rules short enough to memorise. 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, anl_, a u_, or a g_. That is the whole vocabulary.