Grouped List (Headers + Rows)
Problem
Your data is organized into named groups, each with a header row followed by its items. The list must render headers and rows in a single virtualized pass, with different heights for each type.
Solution
Flatten groups into a linear renderable list, then pass a per-index estimator to predict header vs. row heights.
ts
import { createVirtualizer } from '@vielzeug/virtualit';
type FlatRow = { type: 'header'; label: string } | { type: 'row'; data: { id: number; name: string } };
const flatList: FlatRow[] = [
{ type: 'header', label: 'A' },
{ type: 'row', data: { id: 1, name: 'Alice' } },
{ type: 'row', data: { id: 2, name: 'Adam' } },
{ type: 'header', label: 'B' },
{ type: 'row', data: { id: 3, name: 'Bob' } },
];
const virt = createVirtualizer(scrollEl, {
count: flatList.length,
estimateSize: (i) => (flatList[i].type === 'header' ? 32 : 44),
onChange: (virtualItems, totalSize) => {
listEl.style.height = `${totalSize}px`;
listEl.innerHTML = '';
for (const item of virtualItems) {
const row = flatList[item.index];
const el = document.createElement('div');
el.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;`;
if (row.type === 'header') {
el.className = 'group-header';
el.style.height = '32px';
el.textContent = row.label;
} else {
el.className = 'row';
el.style.height = '44px';
el.textContent = row.data.name;
}
listEl.appendChild(el);
}
},
});Pitfalls
- The
estimateSizefunction is called during layout. Avoid expensive per-index lookups inside it — pre-compute header vs. row type into the flat index array during data preparation. - If a header renders at the same DOM height as rows but
estimateSizereturns a different value, the layout desynchronizes and rows drift from their expected positions. - Mutating the groups array in place without calling
update({ count })leaves the virtualizer showing the previous item count. Always callupdateafter data changes.