Overview

You can render arrays of TemplateResults with .map() inside html. For lists that insert, remove, or reorder, use repeat(items, keyFn, templateFn) so existing DOM nodes move with their keys instead of being recreated.

repeat is re-exported from lit-html via mates. For thousands of rows, step up to virtualList from mates-virtual — see /docs/directives/virtual.

.map() vs repeat() vs virtualList

API
Best for
Notes
items.map(…) Short / append-only Simple. May recreate nodes on reorder.
repeat(items, keyFn, tpl) Keyed lists Moves existing nodes — better for mutable lists.
virtualList(…) Huge lists From mates-virtual — only visible rows in the DOM.

Rendering lists with repeat()

.map() works for simple lists, but repeat(items, keyFn, templateFn) is more efficient when items are added, removed, or reordered — it moves existing DOM nodes instead of recreating them.

import { html, atom, renderApp, repeat } from 'mates';

const App = () => {
const items = atom([
{ id: 1, text: 'Learn Mates', done: true },
{ id: 2, text: 'Build something', done: false },
{ id: 3, text: 'Ship it!', done: false },
]);
const newText = atom('');

const toggle = (id) => items.set(list =>
list.map(i => i.id === id ? { ...i, done: !i.done } : i)
);

const add = () => {
if (!newText().trim()) return;
items.set(list => [...list, { id: Date.now(), text: newText(), done: false }]);
newText.set('');
};

const remove = (id) => items.set(list => list.filter(i => i.id !== id));

return () => html`
<div class="m-col m-gap">
<div class="m-flex m-gap-sm m-items-center">
<input
.value=${newText()}
@input=${e => newText.set(e.target.value)}
@keydown=${e => e.key === 'Enter' && add()}
placeholder="Add item…"
class="m-flex-1"
/>
<button class="btn-primary" @click=${add}>Add</button>
</div>
<!-- repeat() is more efficient than .map() for long lists with key-based updates -->
${repeat(items(), i => i.id, i => html`