Overview

Virtualization lives in the separate mates-virtual package (not the core mates bundle). Import virtualList, virtualMasonry, virtualTable, or masonryGrid from mates-virtual.

virtualList renders only visible rows with automatic height measurement — no itemHeight required. The parent of the expression is the scroll host unless you pass { scroller: true }.

virtualMasonry places items into columns using a required getAspectRatio so heights can be derived without measuring every cell. masonryGrid is a non-virtual CSS column-count layout for smaller feeds.

Which API?

API
Virtualized?
Notes
virtualList Yes — flow Vertical (default) or horizontal list. Optional keyFn. Auto-measures item size.
virtualMasonry Yes — masonry Requires getAspectRatio. Optional keyFn, itemSize, gap, scroller, pin.
virtualTable Yes — rows Rows must have id. Headless role=table structure + VTABLE_CLASSES.
masonryGrid No CSS columns; all items in the DOM. Prefer virtualMasonry for large lists.

virtualList — thousands of rows

virtualList(items, keyFn, template) keeps only visible rows in the DOM. Heights are measured automatically via ResizeObserver — no itemHeight option.

import { html, atom, renderApp } from 'mates';
import { virtualList } from 'mates-virtual';

const TOTAL = 5000;

const App = () => {
const items = atom(
Array.from({ length: TOTAL }, (_, i) => ({
id: i,
name: `Item ${i + 1}`,
tag: ['alpha', 'beta', 'gamma', 'delta'][i % 4],
})),
);

return () => html`
<div class="m-col m-gap">
<p class="label m-0">
${items().length.toLocaleString()} items — only visible rows are in the DOM
</p>
<div class="card card-body">
${virtualList(
items(),
(item) => item.id,
(item, index) => html`
<div class="card card-body m-flex m-gap-sm m-items-center m-justify-between">
<span class="label">#${(index + 1).toLocaleString()}</span>
<span class="m-flex-1">${item.name}</span>
<span class="tag">${item.tag}</span>
</div>
`,
)}
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));

virtualMasonry — aspect-ratio columns

virtualMasonry(items, keyFn, template, options) requires getAspectRatio so column heights can be derived. Use a constant ratio (e.g. () => 1) for a uniform card grid.

import { html, atom, renderApp } from 'mates';
import { virtualMasonry } from 'mates-virtual';

const TOTAL = 400;

const App = () => {
const posts = atom(
Array.from({ length: TOTAL }, (_, i) => ({
id: i,
title: `Post ${i + 1}`,
lines: 1 + (i % 6),
color: `hsl(${(i * 53) % 360}, 45%, 38%)`,
})),
);

return () => html`
<div class="m-col m-gap">
<div class="card">
${virtualMasonry(
posts(),
(p) => p.id,
(post) => html`
<div class="card card-body" style="border-left:3px solid ${post.color}">
<strong>${post.title}</strong>
<p class="label m-t-5">${post.lines} lines tall</p>
</div>
`,
{
getAspectRatio: (p) => 1 / (1 + (p.lines - 1) * 0.25),
itemSize: '180px',
gap: '8px',
},
)}
</div>
</div>
`;