Features #
All features are tree-shaken — you only pay for what you import and use.
Quick Reference #
| Feature | Cost (gzipped) | Description |
|---|---|---|
withAsync() |
+4.6 KB | Lazy loading via adapter with placeholders |
withSelection() |
+2.7 KB | Single / multiple item selection with keyboard nav |
withGrid() |
+3.8 KB | 2D grid layout (virtualises by row) |
withTable() |
+5.5 KB | Data table with resizable columns, sortable headers |
withMasonry() |
+3.3 KB | Pinterest-style shortest-lane placement |
withGroups() |
+2.7 KB | Grouped lists with sticky or inline headers |
withScrollbar() |
+1.1 KB | Custom scrollbar UI with auto-hide |
withPage() |
+0.7 KB | Document-level (window) scrolling |
withScale() |
+3.1 KB | Compress scroll space for 1M+ items |
withAutoSize() |
+0.9 KB | Auto-measure items via ResizeObserver (Mode B) |
withSnapshots() |
+0.7 KB | Scroll position save/restore |
withAsync() — Lazy Loading #
import { vlist, withAsync } from 'vlist';
const feed = vlist({
container: '#feed',
item: {
height: 80,
template: (item) => {
if (!item) return `<div class="skeleton"></div>`;
return `<div>${item.title}</div>`;
},
},
})
.use(withAsync({
adapter: {
read: async ({ offset, limit }) => {
const res = await fetch(`/api/items?offset=${offset}&limit=${limit}`);
return res.json(); // { items, total, hasMore }
},
},
loading: { cancelThreshold: 5, preloadThreshold: 2, preloadAhead: 50 },
}))
.build();
withSelection() — Item Selection #
import { vlist, withSelection } from 'vlist';
const list = vlist({
container: '#list',
items: users,
item: {
height: 48,
template: (user, i, { selected }) =>
`<div class="${selected ? 'selected' : ''}">${user.name}</div>`,
},
})
.use(withSelection({ mode: 'multiple', initial: [1, 2] }))
.build();
list.select(5); // select by id
list.deselect(1); // deselect by id
list.toggleSelect(3); // toggle by id
list.selectAll();
list.clearSelection();
list.getSelected(); // → [2, 5]
list.getSelectedItems(); // → [{ id: 2, ... }, { id: 5, ... }]
withGrid() — 2D Grid Layout #
import { vlist, withGrid } from 'vlist';
const gallery = vlist({
container: '#gallery',
items: photos,
item: {
height: 200,
template: (photo) => `<img src="${photo.url}" alt="${photo.title}" />`,
},
})
.use(withGrid({ columns: 4, gap: 16 }))
.build();
withTable() — Data Table #
import { vlist, withTable, withSelection } from 'vlist';
const table = vlist({
container: '#employees',
items: employees,
item: { height: 40, template: () => '' },
})
.use(withTable({
columns: [
{ key: 'name', label: 'Name', width: 220, sortable: true },
{ key: 'email', label: 'Email', width: 280, sortable: true },
{ key: 'department', label: 'Department', width: 140, sortable: true },
{ key: 'role', label: 'Role', width: 180 },
],
rowHeight: 40,
headerHeight: 44,
}))
.use(withSelection({ mode: 'single' }))
.build();
Leverages all of vlist's core — virtualization, element pooling, size caching, scroll compression — and layers column-aware rendering on top. Rows are the unit of virtualization (same as a plain list), so withScale, withAsync, withSelection, and all other features compose unchanged.
withMasonry() — Pinterest Layout #
import { vlist, withMasonry } from 'vlist';
const gallery = vlist({
container: '#gallery',
items: photos,
item: {
height: (index) => photos[index].aspectRatio * 200,
template: (photo) => `<img src="${photo.url}" alt="${photo.title}" />`,
},
})
.use(withMasonry({ columns: 3, gap: 12 }))
.build();
Items flow into the shortest column, creating an organic packed layout with no wasted space. Unlike grid, each item can have a different height.
withGroups() — Grouped Lists #
import { vlist, withGroups } from 'vlist';
// Sticky headers (Telegram-style contact list)
const contacts = vlist({
container: '#contacts',
items: sortedContacts, // must be pre-sorted by group
item: { height: 56, template: (c) => `<div>${c.name}</div>` },
})
.use(withGroups({
getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
headerHeight: 36,
headerTemplate: (letter) => `<div class="header">${letter}</div>`,
sticky: true,
}))
.build();
withScrollbar() — Custom Scrollbar #
import { vlist, withScrollbar } from 'vlist';
const list = vlist({
container: '#list',
items: data,
item: { height: 48, template: renderItem },
})
.use(withScrollbar({
autoHide: true,
autoHideDelay: 1000,
minThumbSize: 20,
showOnHover: true,
hoverZoneWidth: 16,
showOnViewportEnter: true,
}))
.build();
withPage() — Document Scrolling #
import { vlist, withPage, withAsync } from 'vlist';
const blog = vlist({
container: '#articles',
item: { height: 400, template: (post) => `<article>${post.title}</article>` },
})
.use(withPage()) // window scroll instead of container scroll
.use(withAsync({
adapter: { read: async ({ offset, limit }) => fetchPosts(offset, limit) },
}))
.build();
Cannot combine with withScrollbar() or orientation: 'horizontal'.
withScale() — 1M+ Items #
import { vlist, withScale, withScrollbar } from 'vlist';
const bigList = vlist({
container: '#list',
items: generateItems(2_000_000),
item: { height: 48, template: (item) => `<div>${item.id}</div>` },
})
.use(withScale()) // auto-activates above browser limit
.use(withScrollbar({ autoHide: true }))
.build();
withAutoSize() — Auto-Measurement #
Measure items via ResizeObserver for content with unpredictable sizes.
import { vlist, withAutoSize } from 'vlist';
const feed = vlist({
container: '#feed',
items: posts,
item: {
estimatedHeight: 120,
template: (post) => `<article>${post.text}</article>`,
},
})
.use(withAutoSize())
.build();
Items render at the estimated size, then snap to their measured height. Each item is measured once and cached.
withSnapshots() — Scroll Save/Restore #
import { vlist, withSnapshots } from 'vlist';
const list = vlist({ container: '#list', items, item: { height: 48, template: render } })
.use(withSnapshots())
.build();
// Save before navigation
const snapshot = list.getScrollSnapshot();
sessionStorage.setItem('scroll', JSON.stringify(snapshot));
// Restore on return
const saved = JSON.parse(sessionStorage.getItem('scroll') ?? 'null');
if (saved) list.restoreScroll(saved);
Feature Compatibility #
Most features compose freely. This matrix shows the known constraints:
| Table | Grid | Masonry | Groups | Async | Selection | Scale | Scrollbar | Page | Snapshots | AutoSize | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Table | — | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ❌ |
| Grid | ❌ | — | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ❌ |
| Masonry | ❌ | ❌ | — | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| Groups | ✅ | ✅ | ❌ | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Async | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Selection | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ |
| Scale | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ❌ | ✅ | ✅ |
| Scrollbar | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ❌ | ✅ | ✅ |
| Page | ⚠️ | ⚠️ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | — | ✅ | ✅ |
| Snapshots | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| AutoSize | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Symbol | Meaning |
|---|---|
| ✅ | Fully compatible |
| ⚠️ | Works but may need careful styling |
| ❌ | Cannot combine — builder throws or layout breaks |
Key constraints:
- Table ↔ Grid ↔ Masonry — Mutually exclusive layout modes (each provides its own renderer)
- Table + Groups — ✅ Full-width group headers in data tables, sticky headers sit below column header
- Grid + Groups — ✅ Full-width group headers span the grid
- Masonry ↔ Groups — Masonry doesn't support grouped layouts
- Masonry + reverse — Not supported
- Page ↔ Scrollbar — Page uses the native browser scrollbar; builder throws if both are active
- Page ↔ Scale — Page scroll is vertical-only with native overflow; scale requires a controlled scroll container
- Page + horizontal — Page scroll is vertical only
See Also #
- Builder Pattern — How
.use()/.build()works and feature compatibility - Quick Start — Copy-paste examples combining multiple features
- API Reference — All methods and events