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.

B Block .user_card Identity, in snake_case.
E Element .user_card-title A part, joined by one hyphen.
A Attribute [data-state] State, as data.
M Module UserCard.css One block, one file.
01 The 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 – color 573 respondents
  • Accessibility 27% Contrast, by hand, forever.
  • Theming 24% Named cause: duplicated theme definitions.
  • Cognitive overload 17%
  • Custom properties 7% The tool meant to fix the above.
Top pain points – CSS in general 398 respondents
  • Cognitive overload 22% The number one complaint, overall.
  • Tooling 13%
  • Maintenance 9% Up 18 places from last year.
  • Cascade management 5%

“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.

02 Overview

BEAM in a nutshell.

BEAM takes BEM as its foundation, cleans it up, and builds a house out of the best bricks from 17 years of CSS conventions. This is the antidote to Atomic CSS and CSS-in-JS that adopts the features that made them popular, without paying their architecture tax.

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 may save you time when you first write the component, but maintaining it becomes a nightmare. For a dark theme, you need to define dark colors for every component. The second it changes, you have to update all your components. That's O(n) complexity for a simple color swap.

With BEAM, your component always stays .user_card. And after you import theme.css, you get a dark theme automatically as you create any component, with O(1) complexity for swapping colors.

Why snake_case? user_card is one double-click. user-card is two. You will select this token thousands of times. Optimize for the thousands.

HTML id is a document hook, not a styling API. In BEAM, we never use it in CSS. An ID selector has a specificity of 1,0,0. You can never override it with classes without using another id or !important, so we get rid of it.

UserCard.css CSS
/* 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. The hyphen means one thing, everywhere: belongs to.

Element names stay flat. They never encode how deeply the markup nests – next quarter it will nest differently, and a class that encoded the old shape is now a lie.

NavBar.css CSS
/* 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 one will make you forget ugly string concatenations with dozens of classes. State is not a class. State is data.

You've been writing button--outline, button--primary, button--small, button--rounded, button--loading for years. Or, worse, the same conditional approach, but with Tailwind's 2-5 classes per state. A single class definition can easily take 5–10 lines of code this way.

Stop it, get some help. CSS has had attribute selectors since 1998. It's long overdue that we make it a convention. Put state in data-* attributes, not classes.

Button.html HTML
<button class="button" data-variant="outline" data-color="primary" data-size='sm' data-shape='rounded' 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 four-thousand-line components.css that everyone appends to and nobody removes from. The file tree is an index of your UI.

tree Text
src/components/
  UserCard/
    UserCard.tsx     -> renders .user_card
    UserCard.css     -> styles .user_card and nothing else
  PromoBanner/
    PromoBanner.tsx
    PromoBanner.css
03 Origin

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. 2008 OOCSS

    Separate structure from skin.

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

  2. 2009 BEM

    Namespace every component.

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

  3. 2011 SMACSS

    Categorize rules before you write them.

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

  4. 2014 CSS-in-JS

    Scoping is not optional.

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

  5. 2014 ITCSS

    Order the cascade deliberately.

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

  6. 2017 Atomic / utility-first

    Constraints beat freedom.

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

  7. 2020 CUBE CSS

    Compose layout, do not enumerate it.

    Became the l_* primitives and the Binary Rule.

04 Editor optimization

The naming convention is optimized for easy search and select.

A dash is always treated like a word separator. This is why we use underscores within blocks.

In VS Code, a double-click or D treats a dash as a break and an underscore as part of the word. Neovim does the same: ciw and * eat user_card whole, and stop at the hyphen in user-card.

That is the whole reason for snake_case inside each half. user_card-title is two motions, not four. You will select these names thousands of times. Make the motion cheap.

05 The 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 2 CSS
/* 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 contract CSS
/* 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);
}

Semantic tokens are declared once per theme, in a single theme.css file. Components just read var(--bg-surface).

A third theme is one more CSS 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.

Primary Neutral
data-theme="light"

Refraction

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

Primary Neutral
data-theme="dark"

Refraction

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

Primary Neutral
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.

06 Space

Void and mass are different beasts.

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

Void is margin, padding, gap. Shared rhythm. Always --space-*.

Mass is width, height, inset, 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.css CSS
.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 layout HTML
<article class="l_stack user_card" data-gap="4">
  ...
</article>
Right – the card owns its own layout in CSS HTML
<article class="user_card">
  ...
</article>
07 Interpolation

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-paste CSS
font-size: clamp(
  2.25rem,
  calc(2.25rem + (8 - 2.25) * ((100vw - 40rem) / (80 - 40))),
  8rem
);
What you actually meant CSS
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 viewport 1.5rem clamped at minimum
You write
fluid(var(--text-2xl), var(--text-6xl))
You ship
clamp(1.5rem, -0.75rem + 5.625vw, 3.75rem)
08 Rosetta stone

One card, four conventions.

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.html HTML
<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.scss CSS
.promo-card {
  &__title { font-size: 1.5rem; }

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

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

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 told Text
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
10 Full 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.