Lists & Conditionals
Rendering lists is just .map(). Conditionals are ternaries or &&. Re-render the container when data changes - mx updates efficiently.
let { ul, li, button, div } = mx;
let items = ["alpha", "beta"];
let host = dom.div();
function view(){
return div(
ul( ...items.map(x => li(x)) ),
button({ onclick(){ items.push("item " + (items.length+1)); host.render(view()); } }, "Add"),
button({ onclick(){ items.pop(); host.render(view()); } }, "Remove")
);
}
host.render(view());
document.body.render(host);Tip
Re-rendering a container is usually simpler than surgically mutating children. mx updates efficiently.
Card + Slots (composition pattern)
Pass children by name using a plain object. No Shadow DOM required. The Card function checks whether kids is an array or an object to support both default children and named slots.
let { div, h4, p, button } = mx;
let Card = (props = {}, kids = []) => {
let slots = Array.isArray(kids) ? { default: kids } : kids;
return div({ class:"card" },
div({ class:"card__header" }, h4(props.title || "Card") ),
div({ class:"card__body" }, ...(slots.default || []) ),
div({ class:"card__footer" }, ...(slots.footer || []) )
);
};
document.body.render(
Card({ title:"Welcome" }, {
default: [ p("Cards compose apps nicely.") ],
footer: [ button("Close") ]
})
);Data Tables
A sortable table component. Sort state lives in $state so clicking column headers toggles sort direction and re-renders.
define('sortable-table', {
$({ data = [], columns = [] }) {
let { table, thead, tbody, tr, th, td, div } = mx;
let sortKey = (this.$state.sortKey ??= columns[0]?.key);
let asc = (this.$state.asc ??= true);
let sorted = [...data].sort((a, b) => {
let val = asc ? 1 : -1;
return a[sortKey] > b[sortKey] ? val : -val;
});
return table(
thead(tr(...columns.map(col =>
th({ onclick: () => this.$({
sortKey: col.key,
asc: sortKey === col.key ? !asc : true
})},
col.label || col.key,
sortKey === col.key ? (asc ? ' ↑' : ' ↓') : ''
)
))),
tbody(...sorted.map(u =>
tr(...columns.map(col => td(u[col.key])))
))
);
}
});
let users = [
{ name: "Alice", age: 25, city: "NYC" },
{ name: "Bob", age: 30, city: "LA" },
{ name: "Charlie", age: 35, city: "Chicago" },
{ name: "Diana", age: 28, city: "Miami" },
{ name: "Ethan", age: 42, city: "Seattle" }
];
document.body.render(
mx.sortableTable({
columns: [
{ key: "name", label: "Name" },
{ key: "age", label: "Age" },
{ key: "city", label: "City" }
],
data: users
})
);Date Picker
A date picker component using $state to track the viewed month and selected date independently. Navigating months preserves the selection.
define('date-picker', {
$({ selected, onSelect }) {
let { div, button, table, thead, tbody, tr, th, td } = mx;
let picked = (this.$state.picked ??= selected || new Date);
let view = (this.$state.view ??= new Date(picked));
let year = view.getFullYear();
let month = view.getMonth();
let daysInMonth = new Date(year, month + 1, 0).getDate();
let firstDay = new Date(year, month, 1).getDay();
let days = [];
for (let i = 0; i < firstDay; i++) days.push("");
for (let d = 1; d <= daysInMonth; d++) days.push(d);
// Highlight only when viewing the picked date's month
let pickedDay = picked.getFullYear() === year
&& picked.getMonth() === month ? picked.getDate() : 0;
return [
div(
button({ onclick: () => {
view.setMonth(month - 1);
this.$({ view });
}}, "‹"),
`${new Date(year, month).toLocaleString('default',
{ month: 'long' })} ${year}`,
button({ onclick: () => {
view.setMonth(month + 1);
this.$({ view });
}}, "›")
),
table(
thead(tr(...["Su","Mo","Tu","We","Th","Fr","Sa"]
.map(d => th(d)))),
tbody(...Array.from(
{ length: Math.ceil(days.length / 7) }, (_, w) =>
tr(...days.slice(w * 7, (w + 1) * 7).map(day =>
td({
class: day === pickedDay ? "selected" : "",
onclick: () => {
if (!day) return;
this.$({
picked: new Date(year, month, day),
view
});
onSelect?.(new Date(year, month, day));
}
}, day || "")
))
))
)
];
}
});
document.body.render(mx.datePicker());Todo Example
A classic Todo app in mx. State lives in local variables; the entire view re-renders on every change. mx handles minimal DOM updates under the hood.
let { div, h3, input, button, ul, li, span } = mx;
let todos = [];
let text = "";
function TodoApp(){
return div({ class:"todo" },
h3("Todos"),
div(
input({ ".value": text, oninput: e => {
text = e.target.value;
todoHost.render(TodoApp());
}}),
button({ onclick: _ => {
if (text.trim()) {
todos.push({ text, done: false });
text = "";
todoHost.render(TodoApp());
}
} }, "Add")
),
ul( ...todos.map((t, i) =>
li({ class: t.done ? "done" : "" },
input({ type:"checkbox", ".checked": t.done, onchange: _ => {
t.done = !t.done;
todoHost.render(TodoApp());
}}),
span(t.text),
button({ onclick: _ => {
todos.splice(i, 1);
todoHost.render(TodoApp());
} }, "×")
)
))
);
}
let todoHost = dom.div();
todoHost.render(TodoApp());
document.body.render(todoHost);Drag & Drop Reordering
HTML5 drag events work naturally with mx. A reorderable list component tracks the dragged index in $state and splices on drop.
define('drag-list', {
$({ items = [], onReorder }) {
let { div, span } = mx;
return items.map((item, i) =>
div({
class: 'drag-item',
draggable: 'true',
ondragstart: () => { this.$state.dragging = i; },
ondragover: e => e.preventDefault(),
ondrop: () => {
let arr = [...items];
let [moved] = arr.splice(this.$state.dragging, 1);
arr.splice(i, 0, moved);
this.$({ items: arr });
onReorder?.(arr);
}
},
span({ class: 'handle' }, '⋮⋮'),
span(item)
)
);
}
});
document.body.render(
mx.dragList({
items: ['Learn mx', 'Build components', 'Ship to prod', 'Profit']
})
);Tip
Use ondragstart to store the source index, ondragover with e.preventDefault() to allow dropping, and ondrop to splice the array and re-render.
File Drop Zone
A drop zone component combines drag events with a hidden file input. Events are assigned directly on this so re-renders overwrite the same property instead of stacking listeners.
define('drop-zone', {
$({ ondrop, accept }) {
let { div, input, span } = mx;
let handleFiles = files => ondrop?.(Array.from(files));
// Direct assignment - safe on re-render (overwrites, never stacks)
this.ondragover = e => {
e.preventDefault();
this.classList.add('dragging');
};
this.ondragleave = e => {
if (e.target === this) this.classList.remove('dragging');
};
this.ondrop = e => {
e.preventDefault();
this.classList.remove('dragging');
if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
};
return div({ class: 'drop-area',
onclick: _ => this.querySelector('input').click()
},
span({ class: 'drop-text' }, 'Drop files here'),
span({ class: 'drop-hint' }, 'or click to select'),
input({ type:'file', multiple:true, accept,
style: 'display:none',
onchange(e) {
if (e.target.files.length) {
handleFiles(e.target.files);
e.target.value = '';
}
}
})
);
}
});
document.body.render(
mx.dropZone({
accept: 'image/*',
ondrop(files) { alert(files.map(f => f.name).join(', ')); }
})
);Key pattern
Drag events are assigned directly on this (this.ondragover, this.ondrop, etc.) because they need to be on the component element itself. Direct assignment is safe on re-render - it overwrites the previous handler instead of stacking listeners. The visual content is returned as mx children.
Progress Bar (computed attrs)
Components can compute derived values from props and set attributes on themselves via this.$attrs(). This progress bar calculates completion percentage, urgency level, and sets attributes the CSS can target:
define('goal-progress', {
$({ pledged = 0, goal = 1000, deadline }) {
let pct = Math.min(100, Math.round(pledged / goal * 100));
let days = deadline
? Math.ceil((new Date(deadline) - Date.now()) / 86400000)
: null;
// Set attributes for CSS styling
this.$attrs({
completed: pct >= 100 || null,
urgency: days !== null && days <= 3 ? 'high'
: days !== null && days <= 7 ? 'medium'
: null
});
return [
mx.div({ class: 'bar' },
mx.div({ class: 'fill', style: `width:${pct}%` })
),
mx.div({ class: 'stats' },
mx.div(mx.strong('$' + pledged.toLocaleString()),
' of $' + goal.toLocaleString()),
days !== null
? mx.div(mx.strong(days), ' days to go')
: null,
mx.div(mx.strong(pct + '%'), ' funded')
)
];
}
});
document.body.render(
mx.goalProgress({
pledged: 30000, goal: 50000,
deadline: '2026-04-01'
})
);$attrs for CSS hooks
this.$attrs({ urgency: "high" }) sets urgency="high" on the element. Your CSS can then target goal-progress[urgency="high"] for styling. Passing null or false removes the attribute. This is cleaner than toggling class names manually.