Introduction
mx.js is a rendering engine in 849 bytes (brotli). No build steps, no virtual DOM, no framework overhead - just a minimal engine that turns plain JavaScript into efficient DOM.
It powers everything from 6KB marketing sites to enterprise platforms with 200+ components and 4,900+ commits.
Why mx?
- 849 bytes - smaller than most favicons.
- Zero dependencies - no npm install, no node_modules.
- Zero build step - drop a script tag and go.
- 4 globals - that's the entire API surface.
- Real DOM - no virtual DOM abstraction. Elements are elements.
Install
Script tag - works everywhere:
<!-- jsDelivr (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/@tltdsh/mx"></script>
<!-- or unpkg -->
<script src="https://unpkg.com/@tltdsh/mx"></script>
Or from npm:
npm install @tltdsh/mx
That's it. Four globals are now available: mx, dom, define, components.
Hello mx
Create elements and render them:
let { div, h1, p, button } = mx;
let app = div({ class: "app" },
h1("Hello, mx"),
p("This is tiny and friendly."),
button({ onclick(){ alert("Clicked!") } }, "Click me")
);
document.body.render(app);The 4 Globals
The entire API:
| Global | Returns | Use for |
|---|
mx.tag() | Array (description) | Inside render() - reconciled efficiently |
dom.tag() | HTMLElement | Persistent references, calling .$() later |
define(name, {}) | void | Register a component |
components | object | Component registry lookup |
// mx - create descriptions (for render to reconcile)
let heading = mx.h1({ class: 'title' }, 'Hello');
// dom - create real DOM elements (persistent references)
let container = dom.div({ id: 'app' });
document.body.append(container);
// define - register a component
define('my-counter', {
$({ count = 0 }) {
return [
mx.button({ onclick: _ => this.$({ count: count - 1 }) }, '-'),
mx.span(count),
mx.button({ onclick: _ => this.$({ count: count + 1 }) }, '+')
];
}
});
// components - the registry
components['my-counter'] // { $() { ... } }mx() vs dom()
The most important distinction in mx.js:
mx.div() - returns an array (description). Cheap to create, used inside render().dom.div() - returns a real HTMLElement. Use when you need a persistent reference.
// mx.div() - description (array), for inside render()
container.render(
mx.div({ class: 'item' }, 'Created by render')
);
// dom.div() - real element, for persistent references
let sidebar = dom('sidebar-nav', { currentPath: '/' });
document.body.append(sidebar);
// Later, update it directly:
sidebar.$({ currentPath: '/about' });Rule of thumb: use mx inside render() for everything. Use dom only when you need to store a reference and call .$() on it later.
render()
el.render(...children) is the core. It reconciles children left-to-right against existing DOM:
- Same tag → reuse element, update attributes, recurse children
- Different tag → replace element
- String/Number → create/update text node
- null/false → skipped (conditional rendering)
- Real DOM node → identity match (for keyed lists)
let box = dom.div({ id: 'box' });
document.body.append(box);
// First render
box.render(
mx.h1('Title'),
mx.p('Paragraph one'),
mx.p('Paragraph two')
);
// Update - only text changes, elements reused
box.render(
mx.h1('New Title'),
mx.p('Updated paragraph'),
mx.p('Still here')
);Your First Component
Components are registered with define() and used with mx():
define('user-card', {
$({ name = 'Anonymous', role = 'Member', online = false }) {
return mx.div({ class: 'card' },
mx.div({ class: 'card-header' },
mx.strong(name),
online
? mx.span({ class: 'badge online' }, 'online')
: null
),
mx.p({ class: 'card-role' }, role)
);
}
});
// Use it
container.render(
mx('user-card', { name: 'Alice', role: 'Admin', online: true }),
mx('user-card', { name: 'Bob' })
);State updates via this.$({ key: value }) - merges into state and re-renders automatically.
A Complete App
A todo app in ~20 lines - form handling, list rendering, state management:
// A complete mini-app in ~20 lines
define('todo-app', {
$({ items = [], text = '' }) {
return [
mx.h2('Todos (' + items.length + ')'),
mx.form({ onsubmit: e => {
e.preventDefault();
if (!text.trim()) return;
this.$({ items: [...items, text], text: '' });
}},
mx.input({
value: text,
placeholder: 'Add a todo...',
oninput: e => this.$({ text: e.target.value })
}),
mx.button({ type: 'submit' }, 'Add')
),
mx.ul(
...items.map((item, i) =>
mx.li(
mx.span(item),
mx.button({ onclick: _ => {
let next = [...items];
next.splice(i, 1);
this.$({ items: next });
}}, '\u00d7')
)
)
)
];
}
});
document.body.render(mx('todo-app'));Next Steps
You now know the fundamentals. Explore the docs:
- Elements & Render - attributes, events, reconciliation
- Forms - inputs, validation, multi-step wizards
- Components - define, state, slots, composition
- Patterns - tables, virtual scroll, drag & drop
- Architecture - routing, lazy loading, real-time data, scaling to 200+ components