Scroll modes #
vlist 2.7 has three ways to move a list. They differ in who owns the scroll position and how large the content element gets. Pick by input device and list size; the plugins and the public API are the same in all three.
| Mode | Owner of the position | Content element | Item count | Best for |
|---|---|---|---|---|
native (default) |
The browser viewport | Full virtual size | Up to the browser's ~16.7M px element limit | Wheel, keyboard, parent scroll handoff, find-in-page, native scrollbar |
bounded |
The browser viewport, rebased by vlist | A runway of 2× the viewport | Unbounded | Wheel and keyboard driven lists past the element limit |
synthetic (opt-in, vlist/synthetic) |
vlist, from pointer and wheel events | The viewport itself | Unbounded | Touch-heavy lists, any size; stays opt-in in 3.0 |
Native #
import { createVList } from "vlist";
const list = createVList({ container: "#app", items, item: { height: 48, template } });
The list is a normal scrolling element: rows are positioned inside a content element sized to the full virtual height, and the browser scrolls it. Everything the browser does with a scroller keeps working, including the native scrollbar, find-in-page and handing momentum to a parent scroller at the edges. The limit is the element size the browser will lay out, about 16.7 million pixels; past it, positions are clamped and vlist warns once.
Bounded #
const list = createVList({
container: "#app",
items,
item: { height: 48, template },
scroll: { mode: "bounded" },
});
The content element is a runway twice the viewport high. The browser still scrolls
it, and when the scroll offset reaches a quarter or three quarters of the runway vlist
moves a logical origin and resets the offset to the middle, so any number of items fits
without coordinate compression (RFC-012).
scroll.runway sets the multiple (default 2, minimum 1.5).
Known limitation: on touch devices a long native fling can outrun the runway and stall at its edge. Measured on an iPhone SE (iOS 26.6.1) and a Pixel 8a (Android 17), flings reached 145 to 226 percent of a 16× runway. Use synthetic mode for touch-heavy lists. Bounded is deprecated for 3.0 and stays fully supported in 2.x, without runtime warnings (see Migration: v2 to v3).
Synthetic #
import { createVList } from "vlist/synthetic";
import { scrollbar } from "vlist";
import "vlist/styles";
const list = createVList({
container: "#app",
items,
item: { height: 48, template },
scroll: { mode: "synthetic" },
}, [scrollbar()]);
vlist owns the position. The stage is the viewport itself with overflow: clip; pointer
events (touch-action: pan-x pinch-zoom) and wheel events feed a small motion model
with exponential-decay inertia, and rows are placed relative to the owned position.
There is no runway, no rebase and no element-size limit, and touch flings cannot stall
(RFC-014).
What changes for you:
- No native scrollbar. Add
scrollbar(); it applies platform defaults (thin overlay on macOS and Android, classic on Windows), readsscrollbar-widthandscrollbar-colorfrom the container, and is keyboard and screen-reader accessible. - Programmatic scrolls commit synchronously.
scrollTo,scrollToIndexand plugin corrections updategetScrollPosition(), render and emitscrollin the call. Native mode behaves the same from 2.8. - Supported plugins in 2.7: table, groups, snapshots, scrollbar, autosize,
transition, selection, a11y, and from 2.8
page().carousel()andsortable()throw with synthetic mode, as do horizontal lists on right-to-left pages. Vertical lists and tables on right-to-left pages are supported. - Cost: the
vlist/syntheticentry adds about 2.6 KB gzipped to the base; the defaultvlistentry is unchanged for everyone who does not opt in.
Framework adapters (2.8) #
The adapters go through vlist/config. From vlist 2.8 pass the synthetic factory as
factory; the driver is only bundled when you import it:
import { useVList } from "vlist-react";
import { createVList } from "vlist/synthetic";
const { containerRef } = useVList({
factory: createVList,
scroll: { mode: "synthetic" },
items,
item: { height: 48, template: item => String(item.id) },
});
Requesting scroll.mode: "synthetic" without a factory throws at creation with the
import to add.
Choosing #
- Wheel and keyboard only, under the element limit: native.
- Wheel and keyboard only, any size: bounded in 2.x, synthetic from 3.0.
- Touch, any size: synthetic.
- Document scrolling with
page(): native in 2.7; native or synthetic entry in 2.8, within the element limit.
Try all three side by side in the large list example.