Three things to install. None of them is a framework.
BEAM ships as plain CSS you own, one build-time plugin that does arithmetic, and a text file that teaches your coding agent the rules. There is no runtime, no config object, and nothing to keep up to date. If you delete the plugin tomorrow, your stylesheets still work — you just have to write your ownclamp().
Five files you need, and one you might.
These are the exact stylesheets this website is wearing. Not a sanitised example — the real ones, copied out of src/styles at build time so they cannot drift from what you are looking at.
| File | Owns |
|---|---|
reset.css | Browser normalisation. No classes, by rule. |
theme.css | All three token layers. The only file that knows a colour. |
layout.css | The l_* primitives. |
utils.css | The u_* zero-state behaviours. |
generics.css | The g_* global visual objects. |
mkdir -p src/styles && cd src/styles
for f in reset theme layout utils generics; do
curl -sO https://beamcss.dev/starter/$f.css
done/* Order is load-bearing: faces before anything
asks for them, resets before tokens, tokens
before anything that reads them. */
@import './fonts.css'; /* optional */
@import './reset.css';
@import './theme.css';
@import './layout.css';
@import './utils.css';
@import './generics.css';postcss-beam-fluid
It turns fluid(min, max) into a clamp() with the slope pre-computed. That is the entire scope of the package, and it will never grow.
pnpm add -D postcss-beam-fluidnpm install --save-dev postcss-beam-fluidimport postcssBeamFluid from 'postcss-beam-fluid'
export default {
plugins: [
postcssBeamFluid({
minViewport: '40rem',
maxViewport: '80rem',
tokenFiles: ['src/styles/theme.css'],
}),
],
}/* authored */
.hero_title {
font-size: fluid(var(--text-4xl), var(--text-8xl));
}
/* built */
.hero_title {
font-size: clamp(2.25rem, -1.5rem + 9.375vw, 6rem);
}| Option | Default | What it does |
|---|---|---|
minViewport | 20rem | Viewport width at which the minimum value is reached. |
maxViewport | 80rem | Viewport width at which the maximum value is reached. |
tokenFiles | [] | Stylesheets to scan for --custom-property declarations, so fluid(var(--space-4), var(--space-8)) resolves. |
tokens | {} | Inline token map. Wins over tokenFiles, which is handy in tests. |
precision | 4 | Decimal places in the emitted maths. Four is plenty; more is noise. |
Per-call bounds override the project defaults when a single element needs a different curve:fluid(2rem, 8rem, 20rem, 60rem). Anything the plugin cannot resolve statically —em, %, a mixed-unit pair, a token that does not exist — throws and fails the build. That is deliberate. A wrong font size that nobody notices for three months is a worse outcome than a red pipeline.
pnpm build && ! rg -q "fluid\(" distThe agent skill.
Three markdown files: the rules, the token catalogue, and worked examples with their anti-patterns. Drop them where your assistant looks for skills and it will write BEAM without being reminded — and review its own diff against the same checklist you would use.
mkdir -p .cursor/skills/beam-css && cd $_
for f in SKILL.md reference.md examples.md; do
curl -sO https://beamcss.dev/skill/$f
done| Assistant | Where it goes |
|---|---|
| Cursor | .cursor/skills/beam-css/ |
| Claude Code | .claude/skills/beam-css/ |
| Codex | .codex/skills/beam-css/ |
| Anything else | Point it at the three files, or paste SKILL.md into your system prompt. |
Two minutes of editor setup.
One setting matters, and it is the same idea in every editor: a BEAM class is one word, so teach your editor that the hyphen is part of it. Neovim gets this right in CSS and wrong everywhere your markup lives.
-- The css ftplugin already does iskeyword+=-, so
-- user_card-main_title is one word in a stylesheet.
-- The filetypes holding your markup do not, and
-- there * grabs 'user_card' and drops the element.
vim.api.nvim_create_autocmd('FileType', {
pattern = {
'html',
'scss',
'javascriptreact',
'typescriptreact',
'svelte',
'vue',
},
callback = function()
vim.opt_local.iskeyword:append('-')
end,
}){
"editor.wordSeparators": "\\`~!@#$%^&*()=+[{]}\\\\|;:'\\\",.<>/?"
}Both edits do one thing: remove the hyphen from the set of characters that end a word. The underscore is already absent from both defaults, so user_card was never the problem —user_card-main_title was, because the seam between block and element is exactly where your editor wanted to stop.
Fix it and the full name becomes a single object you can grab. Double-click selects all of it.* in Neovim searches for all of it, which is what makescgn then. a complete rename, and⌘D a complete multi-select. This is the entire payoff of the no-concatenation rule, and it is the only editor setting BEAM will ever ask you for.
There is a real trade-off here, so choose rather than drift. Leave the hyphen as a separator andciw renames the block or the element independently, which is lovely — but* can then only ever search one half, and a whole-name search costs a visual selection or typing it out. Add the hyphen and you get the reverse. Whole-name operations are the ones you do across files, so that is the default worth having, but a codebase that renames elements constantly might reasonably decide otherwise.
Beyond that, there is nothing to install. The names are plain strings, so :grep, the quickfix list, Telescope, rg and your language server's symbol list all already understand your stylesheet. For linting, BEAM does not ship a Stylelint plugin, because the rules worth automating are expressible withselector-class-pattern and a grep in CI. Anything more elaborate tends to fight you.
{
"rules": {
"selector-class-pattern": [
"^(l_|u_|g_)?[a-z][a-z0-9_]*(-[a-z][a-z0-9_]*)?$",
{ "message": "Class must be snake_case, optionally block-element, optionally l_/u_/g_ prefixed" }
],
"selector-max-specificity": "0,2,0",
"selector-max-compound-selectors": 2,
"declaration-property-value-disallowed-list": {
"/^(margin|padding|gap)/": ["/^[0-9]/"],
"/^(width|height|inset)/": ["/--space-/"],
"/^transition/": ["/[0-9]+ms/", "/cubic-bezier/"]
}
}
}Your first component.
The whole architecture, in twenty lines. If this reads obvious to you, you already know BEAM — the rest of the specification is just the same idea applied consistently.
<article class="profile_card" data-featured="true">
<div class="l_cluster" data-gap="4">
<img class="profile_card-avatar" src={src} alt="" />
<div class="l_stack" data-gap="1">
<h3 class="profile_card-name">{name}</h3>
<p class="profile_card-role">{role}</p>
</div>
</div>
</article>.profile_card {
padding: var(--space-4);
border: 1px solid var(--border-base);
border-radius: var(--radius-lg);
background: var(--bg-surface);
}
.profile_card[data-featured='true'] {
border-color: var(--border-focus);
}
.profile_card-avatar {
width: 3rem;
height: 3rem;
border-radius: var(--radius-full);
}
.profile_card-name {
color: var(--ink-main);
font-size: var(--text-base);
font-weight: var(--weight-semibold);
}
.profile_card-role {
color: var(--ink-muted);
font-size: var(--text-sm);
}Read it once more and notice what is absent. No colour. No breakpoint. No dark: anything. No margin reaching out to push a neighbour around. The card describes itself and nothing else, which is why you can drop it into a sidebar, a modal or an inverted footer and it will simply behave.