# mx.js > A sub-1KB (brotli) DOM reconciliation engine for building UIs in plain > JavaScript. Four API names, zero dependencies, no virtual DOM, no build step. > Small enough to paste into an AI context window. mx is a reconciler, NOT a > framework: no JSX, no hooks, no signals, no synthetic events. If you are an AI agent writing mx, do not pattern-match it onto React, Vue, or Svelte. That is the source of almost every mistake. Use native DOM names. ## The four API names The classic script puts them on window. The ESM build exports them instead and creates no globals: `import { mx, dom, define, components } from '@tltdsh/mx'`. - `mx.tag(attrs?, ...children)` -> a description ARRAY (`._ === 1`); hand it to `render()`. - `dom.tag(attrs?, ...children)` -> a real element you can keep a handle to (and call `.$()` on, if it is a component). - `define(name, { $(props) {} })` -> register a component. camelCase and kebab-case names both convert to a kebab tag. - `components` -> the component registry object. Decision rule: need to hold a reference and update it later? use `dom`. Just producing output for `render()`? use `mx`. ## Components ``` define('user-card', { $({ name = '', active = false }) { // receives the merged $state return [ mx.span({ class: 'name' }, name), active && mx.span({ class: 'badge' }, 'online') ]; } }); container.render(mx.userCard({ name: 'Ada', active: true })); ``` `$` runs on every `.$()` call and every time a parent render reaches the component through an mx description (`mx.tag(...)`); `dom.tag(attrs)` runs it once at creation, `dom.tag()` with no attrs does not run it until you call `.$()`. A component passed to `render()` as a real node (a kept `dom()` handle) is only placed, not re-run: call `el.$({...})` to update it. It is not an attach/detach hook. Return one mx description OR an array of children (a bare DOM node throws; use `[node]`). A component's attrs all go to `$state`, none become HTML attributes, and children passed to `mx.tag(attrs, ...children)` are ignored. ## State ($state) - `this.$({ key: val })` does `Object.assign($state, { key: val })` then re-runs `$`. - A parent's `el.$({...})` merges the same way. The merge runs BEFORE your `$`, so THE PARENT ALWAYS WINS any key it re-passes. `??=` does not protect against this; seed a private `this._x` for state that must survive re-passes. - Never assign `this.$state.x = v` directly (no merge, no re-render). - `this.$({})` with an empty object is a code smell - pass the actual change. ## Attributes (native DOM names, lowercase) - `onclick: fn` - events lowercase, set as a property. NOT `onClick`. - Custom event types can't use `on*` props (silent expando) - use `addEventListener` once, guarded. - `class: 'x'` - NOT `className`. - `for: 'id'` - NOT `htmlFor`. - `'.value': v` - a leading dot sets a JS property instead of an attribute. - `disabled: true` -> `disabled="true"`; `disabled: null` / `false` / `undefined` -> removed. - `style: 'width:' + pct + '%'` - style is a STRING, never an object. - In `render()`, `value`, `checked`, `selected` set both attribute and property, re-asserted every render. `dom.tag(attrs)` sets the attribute only. ## render() rules - mx description -> create/reuse by tag name, apply attrs, recurse children. - real DOM node -> match by identity (===); moves with its internal state. - string / number -> text node (numbers auto-cast; no `String()` needed). - `false` / `null` / `undefined` -> skipped. `0` is NOT skipped; it renders as "0". - plain array -> flattened into the child list, nested arrays too (a description carries `._` and stays whole). - excess old children are removed. ## The rules agents get wrong - Guard numbers before `&&`: `!!list.length && mx.div(...)`, never `list.length && ...`. - Arrays in children flatten, nested arrays too: `mx.ul(items.map(i => mx.li(i.name)))` and `mx.ul(...items.map(...))` render the same. mx 1.4.1 and earlier stringify an unspread array to "li,,a,li,,b", so spread when targeting those. - No PascalCase tags: `mx.UserCard()` -> `<-user-card>`, which throws `InvalidCharacterError` at render. Use `mx.userCard()`. - Copy prop arrays before sorting: `[...items].sort(...)`, never `items.sort(...)`. - Never `addEventListener` inside `$()` (stacks each render); assign `this.onclick = fn`. - `clearInterval(this._interval)` at the TOP of `$()` before re-arming a timer. - Hoist static option/config arrays OUT of `$()`; don't re-allocate per render. - Stateful list rows: use a keyed `dom()` Map matched by identity, not positional `mx()` rows (positional rows leave typed input at the old index after a sort). - `key` is not special: `mx.li({ key: id }, ...)` writes a plain `key="..."` attribute, rows still match by position (tag name, left to right) and nothing warns. For identity, use the keyed `dom()` Map above. - Drive logic from `$state` / data attributes, never from rendered text (`el.textContent`). - Write `$(props) {}` (or `$: function(props) {}`). Never an arrow `$: props =>`: it loses `this`. A Svelte `$:` label is not reactive. ## Docs - [Getting Started](https://mxjs.dev/getting-started): install via CDN or npm, the four API names, a first component - [Elements & Render](https://mxjs.dev/elements): how mx() and dom() build elements, apply attributes and events, and how render() reconciles - [Components](https://mxjs.dev/components): define(), $state, slots, composition, updates from outside - [State](https://mxjs.dev/state): persistent state, the event bus, cross-component coordination - [Lifecycle](https://mxjs.dev/lifecycle): owning less, isConnected and MutationObserver cleanup, when custom element callbacks help - [Forms](https://mxjs.dev/forms): controlled and uncontrolled inputs, validation, debounced input, multi-step forms - [Patterns](https://mxjs.dev/patterns): mx() vs dom(), keyed dom() Maps, sortable tables, virtual scroll, drag and drop - [API Reference](https://mxjs.dev/reference): mx(), dom(), define(), render(), $attrs(), components - [Cheat Sheet](https://mxjs.dev/cheatsheet): one-page API summary, attributes, render rules - [Gotchas](https://mxjs.dev/gotchas): common mistakes, each with the fix - [For AI agents](https://mxjs.dev/ai): paste-ready system prompt, the React/Vue/Svelte habits that break mx, a self-check list - [Testing](https://mxjs.dev/testing): direct DOM assertions and browser checks - [What mx is not](https://mxjs.dev/what-mx-is-not): what mx deliberately leaves out, and what to use instead ## Source - [Engine](https://mxjs.dev/ui-mx.min.js): the whole reconciler, minified - [Engine, readable source](https://cdn.jsdelivr.net/npm/@tltdsh/mx/skill/ui-mx.js): the whole reconciler, the authority on behaviour - [Agent skill](https://mxjs.dev/mx-skill.zip): these rules plus the readable engine source, installable in a skills folder - [AGENTS.md in the npm package](https://cdn.jsdelivr.net/npm/@tltdsh/mx/AGENTS.md): rules for coding agents - [npm package](https://www.npmjs.com/package/@tltdsh/mx): @tltdsh/mx ## Optional - [Installation](https://mxjs.dev/installation): CDN, npm, ES modules, sizes - [Tutorial](https://mxjs.dev/tutorial): a contact form with validation, step by step - [Accessibility](https://mxjs.dev/accessibility): aria attributes, focus management, keyboard navigation, live regions - [Web Components](https://mxjs.dev/web-components): mx inside custom elements, Shadow DOM, interop - [Architecture](https://mxjs.dev/architecture): routing, lazy loading, real-time data, large apps - [Best practices](https://mxjs.dev/best-practices): events, properties, security, component patterns - [Examples](https://mxjs.dev/examples): small complete apps to read and run - [Performance](https://mxjs.dev/performance): benchmarks against other frameworks - [Changelog](https://mxjs.dev/changelog): what changed in each version