core
Source
// Basic List — React implementation using vlist-react adapter
// Demonstrates core vlist with 100,000 items.
import { createRoot } from "react-dom/client";
import { useVList } from "vlist-react";
import { COUNT, ITEM_HEIGHT, makeItems, itemTemplate } from "../shared.js";
// =============================================================================
// App Component
// =============================================================================
const items = makeItems(COUNT);
function App() {
const { containerRef } = useVList({
ariaLabel: "Orders",
items,
item: {
height: ITEM_HEIGHT,
striped: true,
template: itemTemplate,
},
});
return (
<div className="container">
<header>
<h1>Basic List</h1>
<p className="description">
React implementation — the core of <code>vlist</code>. Scroll
through 100,000 items to see virtualization in action.
</p>
</header>
<div className="split-layout">
<div className="split-main">
<h2 className="sr-only">Orders</h2>
<div ref={containerRef} id="list-container" />
</div>
<aside className="split-panel">
<section className="ui-section">
<h3 className="ui-title">About</h3>
<p className="ui-text">
This list renders <strong>100,000 items</strong> but only creates
DOM nodes for the visible rows. Scroll at any speed — the frame
rate stays constant.
</p>
<p className="ui-text">
Built with <strong>vlist-react</strong>, the React adapter for
vlist. One hook, zero boilerplate.
</p>
</section>
<section className="ui-section">
<h3 className="ui-title">How it works</h3>
<p className="ui-text">
Each item has a fixed height of <strong>64 px</strong>. vlist
calculates which rows are visible and renders only those,
recycling DOM elements as you scroll.
</p>
</section>
<section className="ui-section">
<h3 className="ui-title">Accessibility</h3>
<p className="ui-text">
vlist implements the <strong>WAI-ARIA Listbox</strong> pattern to
provide a fully accessible virtual list experience — including{" "}
<code>role</code>, <code>aria-setsize</code>,{" "}
<code>aria-posinset</code>, and keyboard navigation out of the
box.
</p>
</section>
</aside>
</div>
</div>
);
}
// =============================================================================
// Mount
// =============================================================================
createRoot(document.getElementById("react-root")!).render(<App />);
<div id="react-root"></div>
// Shared data and utilities for basic list example variants
// This file is imported by all framework implementations to avoid duplication
// =============================================================================
// Constants
// =============================================================================
export const COUNT = 100_000;
export const ITEM_HEIGHT = 64;
// =============================================================================
// Data generator
// =============================================================================
// Deterministic hash (murmurhash-inspired)
const hash = (i, seed = 0) => {
let h = Math.imul(i ^ seed ^ 0x5bd1e995, 0x5bd1e995);
h ^= h >>> 13;
h = Math.imul(h, 0x5bd1e995);
return (h ^ (h >>> 15)) >>> 0;
};
const pick = (arr, i, seed) => arr[hash(i, seed) % arr.length];
const STATUSES = [
"shipped",
"delivered",
"pending",
"processing",
"cancelled",
"returned",
];
// Date clustering — each day gets a random batch of orders (50–350)
const TODAY = new Date();
TODAY.setHours(0, 0, 0, 0);
const DAY = 24 * 60 * 60 * 1000;
const formatDate = new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
}).format;
// Pre-build a date for each order index (0 = most recent)
const buildDates = (count) => {
const dates = new Array(count);
let idx = 0;
let day = 0;
while (idx < count) {
const batch = (hash(day, 99) % 301) + 50; // 50–350 orders per day
const base = TODAY.getTime() - day * DAY;
const end = Math.min(idx + batch, count);
for (let j = idx; j < end; j++) dates[j] = base + (hash(j, 88) % DAY);
idx = end;
day++;
}
return dates;
};
let ORDER_DATES = buildDates(COUNT);
const ensureDates = (count) => {
if (count > ORDER_DATES.length) ORDER_DATES = buildDates(count);
};
const formatAmount = new Intl.NumberFormat(undefined, {
style: "currency",
currency: "USD",
}).format;
/**
* Generate a single order item by index.
* Deterministic — same index always produces the same item.
* @param {number} i - 0-based item index (order id = i + 1)
* @returns {{ id: number, customer: number, amount: string, date: string, status: string }}
*/
export const makeItem = (i) => {
const status = pick(STATUSES, i, 1);
const amount = ((hash(i, 2) % 999900) + 100) / 100; // 1.00 – 9,999.99
const customer = (hash(i, 4) % 90000) + 10000; // 10000–99999
ensureDates(i + 1);
return {
id: i + 1,
customer,
amount: formatAmount(amount),
date: formatDate(ORDER_DATES[i] ?? Date.now()),
status,
};
};
/**
* Generate a batch of order items, highest id first (most recent at top).
* @param {number} count - number of items
* @param {number} [startIndex=0] - base index for id generation
* @returns {Array}
*/
export const makeItems = (count, startIndex = 0) => {
ensureDates(count);
return Array.from({ length: count }, (_, i) => {
const itemIndex = startIndex + count - 1 - i;
return {
...makeItem(itemIndex),
date: formatDate(ORDER_DATES[i]),
};
});
};
// =============================================================================
// Template
// =============================================================================
/** Map order status → ui-badge semantic color */
const STATUS_BADGE = {
shipped: "info",
delivered: "success",
pending: "warning",
processing: "purple",
cancelled: "error",
returned: "muted",
};
export const itemTemplate = (item, i) => `
<div class="item__row">
<span class="item__label">Order #${item.id} — C-${item.customer}</span>
<span class="ui-badge ui-badge--sm ui-badge--${STATUS_BADGE[item.status] || "muted"}">${item.status}</span>
</div>
<div class="item__row">
<span class="item__date">${item.date}</span>
<span class="item__amount">${item.amount}</span>
</div>
`;
/* Basic List — order list item styles (shared across all variants) */
.vlist-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
gap: 4px;
padding: 0 16px;
font-variant-numeric: tabular-nums;
}
.item__row {
display: flex;
align-items: baseline;
gap: 8px;
}
.item__label {
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.item__date {
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
flex: 1;
min-width: 0;
}
.item__amount {
font-size: 15px;
white-space: nowrap;
flex-shrink: 0;
text-align: right;
}
/* ============================================================================
Selected state
============================================================================ */