# behaviors — the opt-in JavaScript sidecar

Landed empty, deliberately: this directory existed before the first behaviour landed in it, so
that behaviour was judged against a budget and a loader convention that already existed, rather
than ones invented to fit it. See CLAUDE.md rule 7 and ROADMAP.md's Phase 0. Five behaviours ship
today — `countdown.js`, `drawer.js`, `highlight.js`, `menu.js`, `tabs.js` — each still held to that same
budget and loader convention.

## What lives here

One plain ES module per component: `src/behaviors/<component>.js`, named for the component it
upgrades. A drawer's script is `drawer.js`; a countdown's is `countdown.js`. Nothing else goes
in this directory — no shared runtime, no shared utility module, no framework. A behaviour must
not import anything outside its own file, and it does nothing beyond wiring the one component it
names. It finds its component by class, the same way any of `src/components/*.css` would be
selected in a stylesheet.

## How a consumer loads one

A second `<script>` tag, added only when the behaviour is wanted:

```html
<link rel="stylesheet" href="node_modules/@arag/css/src/index.css">
<script type="module" src="node_modules/@arag/css/src/behaviors/drawer.js"></script>
```

`src/index.css` never loads a script and never needs one — it is still, on its own, a complete
and working install of every component. The second tag is the same opt-in shape `src/fonts.css`
already uses for webfonts: link order does not matter, and leaving the tag off costs nothing but
the behaviour it would have added.

## The fallback contract

Every component renders, reads and is usable with its script absent or failed. A drawer with no
script is an open `<dialog>`; a countdown with no script is the date, written out as text. The
script upgrades a working component; it never gates one. The test is literal: load the page with
scripting disabled in the browser and use the component by hand. If it does not work, the
component is wrong — not the rule, and not an excuse to add a `<noscript>` fallback markup path
instead of fixing the component.

## The budget

Two ceilings, both minified: 2 KB per behaviour, 5 KB per published page — counting only the
behaviours a block on that page actually declares, not every behaviour in the directory.
`node scripts/budget.mjs --check` enforces both; `npm run check` runs it. There is no minifier in
this repo, so the script uses a conservative, over-estimating approximation instead — see its own
header for exactly what that approximation is and why it can be trusted.

## No framework, no bundler, no dependencies

If a behaviour needs a library to do its job, the component the behaviour upgrades is the wrong
shape — narrow the job, do not add the dependency. Native platform features come first:
`<details>`, `<dialog>`, `popover`, scroll-snap and `<video>` already do most of what a component
would otherwise need script for. A behaviour closes a gap the platform genuinely leaves; it does
not reimplement one the platform already covers.

## How a CMS block ships one

A block type declares `"behavior": "<name>"` on its entry in `cms/blocks.json`, and `cms/render.js`
emits exactly that one `<script type="module">` tag on any published page where a block of that
type appears — the same renderer paints the editor canvas and the published output, so there is
no second code path to keep in sync. A page with no such block on it carries zero bytes of script.
