/ Docs

API Reference #

PublishedUpdated Sep 16, 2026

createVList(config, plugins?) #

Creates a virtual list instance.

import { createVList } from "vlist";

const list = createVList(config, [plugin1(), plugin2()]);

Config #

Option Type Default Description
container HTMLElement | string required DOM element or CSS selector
item.height number | (index, context?) => number — Fixed or per-index item height
item.width number | (index) => number — Fixed or per-index item width (horizontal mode)
item.estimatedHeight number — Auto-measure via ResizeObserver (Mode B)
item.estimatedWidth number — Auto-measure for horizontal (Mode B)
item.template (item, index, state) => string | HTMLElement required Render function
item.gap number 0 Spacing between items (px)
item.striped boolean | "data" | "even" | "odd" false Zebra stripe classes
items T[] [] Initial dataset
overscan number 3 Extra items rendered offscreen
orientation "vertical" | "horizontal" "vertical" Scroll axis
padding number | [number, number] | [t, r, b, l] 0 Container padding (px)
classPrefix string "vlist" CSS class prefix
reverse boolean false Reverse scroll direction
ariaLabel string — Container aria-label
scroll.wheel boolean true Mouse wheel scrolling
scroll.gutter "auto" | "stable" "auto" Scrollbar space reservation
scroll.idleTimeout number 150 Idle detection timeout (ms)
scroll.scrollbar "native" | "none" | ScrollbarOptions "native" Browser scrollbar visibility; the strings are rejected with scroll.mode: "synthetic"
scroll.mode "auto" | "native" | "synthetic" "auto" Who owns scroll input. "auto" is native, and hands a list past the browser's size limit to synthetic input in place

createVList scrolls natively by default and switches to synthetic input by itself past the browser's element size limit. scroll.mode pins either one; the rest of the config is the same. See Scroll modes.

Instance Properties #

Property Type Description
element HTMLElement Root DOM container
items readonly T[] Current item array
total number Total item count (includes virtual items from plugins)

Instance Methods #

Data #

Method Signature Description
setItems (items: T[]) => void Replace entire dataset
appendItems (items: T[]) => void Add items to end
prependItems (items: T[]) => void Add items to beginning
insertItem (item: T, index?: number) => void Insert at position (default: end)
updateItem (id: string | number, updates: Partial<T>) => void Partial update by ID
removeItem (id: string | number) => void Remove by ID
removeItems (ids: ReadonlyArray<string | number>) => number Bulk remove, returns count
getItemAt (index: number) => T | undefined Get item at index
getIndexById (id: string | number) => number Get index by ID (-1 if not found)

Scroll #

Method Signature Description
scrollToIndex (index: number, align?) => void Scroll to item
getScrollPosition () => number Current scroll offset (px)

scrollToIndex accepts a string alignment or an options object:

// Simple alignment
list.scrollToIndex(50, "center");

// Smooth scroll
list.scrollToIndex(50, { align: "end", behavior: "smooth", duration: 300 });

// Custom easing
list.scrollToIndex(50, {
  align: "start",
  behavior: "smooth",
  duration: 800,
  easing: (t) => 1 - Math.pow(1 - t, 3), // ease-out cubic
});

// Elastic easing (overshoot + settle)
const elasticOut = (t) => {
  if (t === 0 || t === 1) return t;
  return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1;
};
list.scrollToIndex(999, { behavior: "smooth", duration: 800, easing: elasticOut });

When easing is omitted, a default easeInOutQuad curve is used.

Events #

Method Signature Description
on (event, handler) => Unsubscribe Subscribe (returns unsubscribe fn)
off (event, handler) => void Unsubscribe

Lifecycle #

Method Signature Description
destroy () => void Tear down instance, remove DOM, clean up listeners

With an async adapter the list does not know how long it is until its first read returns, so a scrollToIndex given before that has no row to scroll to. It is not dropped: the request is held and honoured on the first render that has a total. That makes "open this list on row 2960" safe to call as soon as the list exists, which is the only moment a caller reliably has.

Only the most recent request is held, and only until it is used.

Events #

Core #

Event Payload Description
item:click { item, index, event: MouseEvent } Item clicked
item:dblclick { item, index, event: MouseEvent } Item double-clicked
item:contextmenu { item, index, event: MouseEvent } Item right-clicked
scroll { scrollPosition, direction: "up" | "down" | "left" | "right" } Scroll position changed
scroll:idle { scrollPosition } Scrolling stopped
scroll:mode { mode: "native" | "synthetic" } Input swapped in place (scroll.mode, see Scroll modes)
velocity:change { velocity, reliable } Scroll velocity changed
range:change { range: { start, end } } Visible range changed
resize { width, height } Container resized
data:change { type: "insert" | "add" | "remove" | "update", id } Data mutated
error { error, context, viewport? } Error occurred (see ErrorViewportSnapshot)
destroy — Instance destroyed

Selection Plugin #

Event Payload
selection:change { selected: (string | number)[], items: T[] }
focus:change { id: string | number, index: number }
delete { selected: (string | number)[], items: T[] }

Async Plugin #

Event Payload
load:start { offset, limit }
load:end { items, total? }

Transition Plugin #

Event Payload
remove:end { id: string | number }

Table Plugin #

Event Payload
column:resize { key, index, previousWidth, width }
column:sort { key, index, direction: "asc" | "desc" | null }

Sortable Plugin #

Event Payload
sort:start { index }
sort:move { fromIndex, currentIndex }
sort:end { fromIndex, toIndex }
sort:cancel { originalItems }

Types #

VListItem #

interface VListItem {
  id: string | number;
  [key: string]: unknown;
}

ItemTemplate #

type ItemTemplate<T> = (item: T, index: number, state: ItemState) => string | HTMLElement;

interface ItemState {
  selected: boolean;
  focused: boolean;
}

ScrollToOptions #

interface ScrollToOptions {
  align?: "start" | "center" | "end";
  behavior?: "auto" | "smooth";
  duration?: number;
  easing?: (t: number) => number;
}

The easing function receives a normalized time t (0–1) and returns a normalized progress value. The default is easeInOutQuad. Common easing functions:

Easing Function
Linear t => t
Ease-out cubic t => 1 - Math.pow(1 - t, 3)
Ease-in-out quad t => t < 0.5 ? 2*t*t : 1 - (-2*t+2)**2/2 (default)
Elastic out t => Math.pow(2,-10*t) * Math.sin((t-0.075)*2*Math.PI/0.3) + 1

ScrollSnapshot #

interface ScrollSnapshot {
  index: number;
  offsetInItem: number;
  total?: number;
  dataIndex?: number;
  dataTotal?: number;
  offsetRatio?: number;
  selectedIds?: (string | number)[];
  focusedId?: string | number;
  scrollTop?: number;
  scrollRatio?: number;
}

VListAdapter (async plugin) #

interface VListAdapter<T> {
  read(params: AdapterParams): Promise<AdapterResponse<T>>;
}

interface AdapterParams {
  offset: number;
  limit: number;
  cursor: string | undefined;
  signal: AbortSignal;
}

interface AdapterResponse<T> {
  items: T[];
  total?: number;
  cursor?: string;
  hasMore?: boolean;
}

ScrollbarOptions #

interface ScrollbarOptions {
  autoHide?: boolean;
  autoHideDelay?: number;
  minThumbSize?: number;
  showOnHover?: boolean;
  hoverZoneWidth?: number;
  showOnViewportEnter?: boolean;
}

VListPlugin (custom plugins) #

interface VListPlugin<T extends VListItem = VListItem> {
  readonly name: string;
  readonly priority?: number;
  readonly conflicts?: readonly string[];

  setup?(ctx: PluginContext<T>): void;

  hooks?: {
    onCalculate?(state: EngineState): void;
    onCommit?(state: EngineState): void;
    onAfterScroll?(scrollPosition: number, direction: number): void;
    onIdle?(): void;
    onResize?(width: number, height: number): void;
  };

  destroy?(): void;
}

See the Plugin Authoring tutorial for the full PluginContext interface and working examples, or Exports for all vlist/internals utilities.

Complete Type Reference #

For additional types (ViewportState, SelectionState, EngineState, GridSizeContext, etc.), see Types.