/ Examples
core

Basic List

Pure vanilla JavaScript — the core of vlist. Scroll through 100,000 items to see virtualization in action.

Orders

Source
// Basic List — Minimal vanilla example
// Demonstrates core vlist with 100,000 items.

import { vlist } from "vlist";
import { COUNT, ITEM_HEIGHT, makeItems, itemTemplate } from "../shared.js";

// =============================================================================
// Create list
// =============================================================================

const items = makeItems(COUNT);

const list = vlist({
  container: "#list-container",
  ariaLabel: "Orders",
  items,
  padding: 8,
  item: {
    height: ITEM_HEIGHT,
    striped: true,
    template: itemTemplate,
  },
}).build();
<div class="container">
    <header>
        <h1>Basic List</h1>
        <p class="description">
            Pure vanilla JavaScript — the core of <code>vlist</code>.
            Scroll through 100,000 items to see virtualization in action.
        </p>
    </header>

    <div class="split-layout">
        <div class="split-main">
            <h2 class="sr-only">Orders</h2>
            <div id="list-container"></div>
        </div>

        <aside class="split-panel">
            <section class="ui-section">
                <h3 class="ui-title">About</h3>
                <p class="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 class="ui-text">
                    The entire bundle weighs <strong>~10.4 KB</strong> gzipped,
                    including vlist and this page's code.
                </p>
            </section>

            <section class="ui-section">
                <h3 class="ui-title">How it works</h3>
                <p class="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 class="ui-section">
                <h3 class="ui-title">Accessibility</h3>
                <p class="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>
// 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
   ============================================================================ */