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:

GlobalReturnsUse for
mx.tag()Array (description)Inside render() - reconciled efficiently
dom.tag()HTMLElementPersistent references, calling .$() later
define(name, {})voidRegister a component
componentsobjectComponent 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() - 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:

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: