Package Exports
- @tltdsh/mx
Readme
mx.js
Sub-1KB reactive DOM reconciliation engine for building massive platforms and user interfaces at scale. 849 bytes brotli. 4 globals, zero dependencies, no virtual DOM, no build step. Small enough to fit in an AI context window.
Install
CDN (recommended)
<!-- jsDelivr (multi-CDN, recommended) -->
<script src="https://cdn.jsdelivr.net/npm/@tltdsh/mx"></script>
<!-- unpkg -->
<script src="https://unpkg.com/@tltdsh/mx"></script>Pinned version
<script src="https://cdn.jsdelivr.net/npm/@tltdsh/mx@1.0.9/mx.min.js"></script>npm / yarn / bun
npm i @tltdsh/mxESM (bundler)
import { mx, dom, define, components } from '@tltdsh/mx'ESM (dynamic import from CDN)
const { mx, dom, define, components } = await import('https://cdn.jsdelivr.net/npm/@tltdsh/mx/mx.mjs')Works in any modern browser or Deno - no install, no bundler, one line.
4 Globals
| Global | Returns | Use for |
|---|---|---|
mx.tag(attrs?, ...children) |
Description array | Inside render() - efficient reconciliation |
dom.tag(attrs?, ...children) |
HTMLElement | Persistent references, calling .$() later |
define(name, { $() {} }) |
void | Register a component |
components |
object | Component registry |
CamelCase auto-converts to kebab-case: mx.priceChart() → <price-chart>.
Quick start
define('counter', {
$({ count = 0 }) {
return [
mx.button({ onclick: _ => this.$({ count: count + 1 }) }, '+'),
mx.span(count),
mx.button({ onclick: _ => this.$({ count: count - 1 }) }, '-')
]
}
})
document.body.render(mx.counter({ count: 10 }))Examples
SVG sparkline
define('sparkline', {
$({ history = [], width = 80, height = 24 }) {
if (!history.length) return []
let min = Math.min(...history), max = Math.max(...history)
let range = max - min || 1
let points = history.map((v, i) =>
(i / (history.length - 1) * width).toFixed(1) + ',' +
(height - ((v - min) / range * (height - 2) + 1)).toFixed(1)
).join(' ')
return mx.svg({ viewBox: '0 0 ' + width + ' ' + height, width, height, fill: 'none' },
mx.polyline({ points, stroke: '#0071e3', 'stroke-width': '1.5', 'stroke-linecap': 'round' })
)
}
})Sortable table
define('data-table', {
$({ rows = [], sortKey = 'name', sortDir = 1 }) {
let sorted = [...rows].sort((a, b) => {
let va = a[sortKey], vb = b[sortKey]
return sortDir * (typeof va === 'string' ? va.localeCompare(vb) : va - vb)
})
let header = (label, key) =>
mx.th({ onclick: _ => this.$({ sortKey: key, sortDir: sortKey === key ? -sortDir : 1 }) },
label, sortKey === key ? (sortDir > 0 ? ' ▲' : ' ▼') : null
)
return mx.table(
mx.thead(mx.tr(header('Name', 'name'), header('Value', 'value'))),
mx.tbody(...sorted.map(r => mx.tr(mx.td(r.name), mx.td(r.value))))
)
}
})Keyed list with persistent DOM
define('drink-builder', {
$({ drinks = [], onadd, onremove }) {
this._rowMap ||= new Map
for (let [id] of this._rowMap)
if (!drinks.find(d => d.id === id)) this._rowMap.delete(id)
for (let d of drinks) {
let el = this._rowMap.get(d.id)
if (!el) { el = dom.drinkRow(); this._rowMap.set(d.id, el) }
el.$({ drink: d, onremove })
}
return [
!!drinks.length && mx.div({ class: 'list' },
...drinks.map(d => this._rowMap.get(d.id))
),
mx.button({ onclick: _ => onadd?.() }, '+ Add drink')
]
}
})Toast notifications
let toastContainer = null
function toast(message, type = 'info') {
if (!toastContainer) {
toastContainer = dom.div({ class: 'toast-container' })
document.body.append(toastContainer)
}
let el = dom.div({ class: 'toast toast-' + type },
mx.span(message),
mx.button({ onclick() { el.remove() } }, '×')
)
toastContainer.append(el)
setTimeout(_ => el.remove(), 4000)
}
toast.success = msg => toast(msg, 'success')
toast.error = msg => toast(msg, 'error')How it works
render(...children) reconciles children left-to-right against existing DOM:
mx.*()descriptions → create/reuse elements by tag, apply attrs, recurse- Real DOM nodes → identity-match by reference (keyed lists)
- Strings / numbers → text nodes (auto-cast, no
String()needed) null/false→ skipped (conditional rendering)
Excess old nodes are removed. That's it.
Attributes
mx.div({ class: 'active' }) // setAttribute
mx.input({ '.value': text }) // el.value = text (property)
mx.button({ disabled: true }) // setAttribute(name, '')
mx.button({ disabled: null }) // removeAttribute
mx.button({ onclick: handler }) // el.onclick = handler (auto-cleaned)value, checked, selected set both attribute and property automatically.
Keyed lists
Create persistent elements with dom(), store in a Map, render by reference:
let rowMap = new Map(
data.map(d => [d.id, dom.tableRow({ data: d })])
)
// Nodes move by identity - internal state preserved on reorder
container.render(...data.map(d => rowMap.get(d.id)))Tips
mx.*= descriptions (arrays) for insiderender()dom.*= real HTMLElements for persistent references$()always returns array of children??=protects$statefrom parent prop overwrites- Never
addEventListenerinside$()- use direct property assignment - Numbers auto-cast - no
String()needed - Use
_ =>for unused arrow params clearInterval(this._interval)at top of$()for timersthis._cache ||= new Mapfor persistent Maps
Limits
render(...children) uses rest parameters, which are subject to the browser's maximum function argument count. This varies: ~65,536 in V8 (Chrome/Edge/Node), ~500,000 in SpiderMonkey (Firefox), ~65,536 in JavaScriptCore (Safari). If you're rendering more than ~60K children, pass an array and spread it, or use a DocumentFragment. In practice this only matters for flat lists with tens of thousands of items - use virtual scrolling at that scale instead.
TypeScript
Full type definitions included. Provides autocomplete for all HTML/SVG elements and attributes.
import { mx, dom, define, type MxChild, type MxAttrs } from '@tltdsh/mx'License
0BSD - do whatever you want.