API Reference #
Published Updated 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)
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;
}
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
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;
}
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 .
← Previous
Getting Started
Next →
Scroll modes