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.
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.
2008OOCSS
Separate structure from skin.
Became Mass vs Void: object shape and page rhythm are different budgets.
2009BEM
Namespace every component.
Kept the idea, dropped the typing tax of __ and --.
2011SMACSS
Categorize rules before you write them.
Became the prefix taxonomy: l_, u_, g_, and the block itself.
2014CSS-in-JS
Scoping is not optional.
Kept co-location, refused the runtime and the generated class names.
2014ITCSS
Order the cascade deliberately.
Handed the job to :where() and a flat file layout instead.
2017Atomic / utility-first
Constraints beat freedom.
Kept the constrained scale, put it in tokens instead of the class attribute.
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{}
A
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
<buttonclass="button"data-variant="primary"data-state="loading">
Save
</button>
M
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
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(100);--palette-stone-900:oklch(21.6%0.00656.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 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.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);}
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.
Every color is declared twice: once light, once dark.
Every card repeats all fourteen classes.
Renaming the component renames nothing.
"Which of these 40 divs is the card?" – you, in March
Solved scoping, then billed a runtime for it. Styles in the JS bundle, class names hashed, CSS needs JavaScript to exist.
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
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.