Scroll modes #
vlist 3.0 has two ways to move a list, chosen by the entry you import. They differ in who owns the scroll position and how large the content element gets. Plugins and the public API are the same with both.
| Entry | Owner of the position | Content element | List size | Best for |
|---|---|---|---|---|
vlist (default) |
The browser viewport | Full virtual size | Up to the browser's element size limit, about 16 million px | Native scrollbar, native touch momentum, parent scroll handoff, find-in-page |
vlist/synthetic (opt-in) |
vlist, from pointer, wheel and keyboard events | The viewport itself | Unbounded | Huge lists and application-owned touch motion |
Bounded mode, scroll.mode, scroll.runway and scale() were removed in 3.0. See
Migration: v2 to v3, or the 2.x scroll modes page.
Native #
import { createVList } from "vlist";
const list = createVList({ container: "#app", items, item: { height: 48, template } });
The list is a normal scrolling element: rows sit 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. scroll.scrollbar: "none" hides the browser scrollbar,
and scrollbar() replaces it with a custom one.
The limit is the element size the browser will lay out. When content grows past
16,000,000 px, the list emits an error event once, with the context
content:size:overflow, pointing to vlist/synthetic.
Synthetic #
import { createVList } from "vlist/synthetic";
import { scrollbar } from "vlist";
import "vlist/styles";
const list = createVList({
container: "#app",
items,
item: { height: 48, template },
}, [scrollbar()]);
Importing the factory selects synthetic input; there is no option to set. vlist owns the
position. The viewport clips its content, pointer events (touch-action: pan-x pinch-zoom
on vertical lists), wheel and keyboard events feed a small motion model with
exponential-decay inertia, and rows are placed relative to the owned position. There is no
element-size limit (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. The entry rejects the"native"and"none"scrollbar strings. - Programmatic scrolls commit synchronously.
scrollTo,scrollToIndexand plugin corrections updategetScrollPosition(), render and emitscrollin the call, as with the native entry. - Plugins: every plugin works with this entry, including
carousel(),sortable()andpage(). Horizontal lists on right-to-left pages throw at creation — in this entry and invlist; vertical lists and tables on right-to-left pages are supported in both.page()keeps native document scrolling with either entry. Plugin conflicts are unchanged. - Boundaries: same-axis touch stops at the list's edges without handing off to the
parent page. Use
vlistwhen boundary gestures must scroll the page. - Cost: the
vlist/syntheticentry adds 2.6 KB gzipped to the base; the defaultvlistentry does not include the driver.
Framework adapters #
The adapters go through vlist/config. 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,
items,
item: { height: 48, template: item => String(item.id) },
});
Measured on 3.0.0-next.3 #
Vanilla lists, three runs each, median reported. The window was on a MacBook built-in display running at 120 Hz. Browsers: Chrome 153, Chromium 130, Firefox 156, Safari 26.4.
Inside one browser, native and synthetic match. The gap between browsers is the frame rate that browser delivered, not a difference between the two entries. Dropped frames were 0% and position lag was 0 px on every scroll that completed.
Initial render #
Median time to create the list, in milliseconds.
| Items | Mode | Chrome | Chromium | Firefox | Safari |
|---|---|---|---|---|---|
| 10K | Native | 0.6 ms | 0.9 ms | 1 ms | 2 ms |
| 10K | Synthetic | 0.6 ms | 0.7 ms | 1 ms | 2 ms |
| 100K | Native | 0.6 ms | 0.8 ms | 2 ms | 2 ms |
| 100K | Synthetic | 0.6 ms | 0.8 ms | 2 ms | 2 ms |
| 1M | Native | 1.5 ms | 3.7 ms | 10 ms | 5 ms |
| 1M | Synthetic | 1.4 ms | 3.8 ms | 12 ms | 5 ms |
Scroll #
The scroll test moves about 36,000 px. It does not walk the whole list, so a native 1M run is not a test of the element-size clamp.
| Items | Mode | Chrome | Chromium | Firefox | Safari |
|---|---|---|---|---|---|
| 10K | Native | 120 fps | 120 fps | 30 fps | 60 fps |
| 10K | Synthetic | 120 fps | 120 fps | 30 fps | 60 fps |
| 100K | Native | 120 fps | 120 fps | 30 fps | 60 fps |
| 100K | Synthetic | 120 fps | 120 fps | 30 fps | 60 fps |
| 1M | Native | 120 fps | 120 fps | — | 60 fps |
| 1M | Synthetic | 120 fps | 120 fps | 30 fps | 60 fps |
Frame time at p95 follows that rate: about 9.2 ms in Chrome and Chromium, 18 ms in Safari, 34 ms in Firefox. Native and synthetic stay on the same figure.
Firefox, native, 1M did not scroll. The benchmark reported that the scroll driver did not move the list, on three attempts. Synthetic at 1M did scroll.
Scroll to an index #
Median time for a scrollTo, in milliseconds. The time depends on the browser and does not depend on the entry or the list length.
| Items | Mode | Chrome | Chromium | Firefox | Safari |
|---|---|---|---|---|---|
| 10K | Native | 41 ms | 41 ms | 166 ms | 81 ms |
| 10K | Synthetic | 41 ms | 41 ms | 166 ms | 81 ms |
| 100K | Native | 42 ms | 41 ms | 166 ms | 81 ms |
| 100K | Synthetic | 41 ms | 41 ms | 166 ms | 82 ms |
| 1M | Native | 42 ms | 41 ms | 167 ms | 80 ms |
| 1M | Synthetic | 41 ms | 41 ms | 166 ms | 82 ms |
Memory #
performance.memory exists in Chrome and Chromium only. Firefox and Safari saved a Memory row with status 0 and no heap numbers. Those rows are not a measurement.
Heap allocated by creating the list, in MB. Native and synthetic match. The resident heap is noisier, because a later GC moves it, so it is a poor comparison.
| Items | Mode | Chrome | Chromium |
|---|---|---|---|
| 10K | Native | 0.07 MB | 0.11 MB |
| 10K | Synthetic | 0.08 MB | 0.13 MB |
| 100K | Native | 0.41 MB | 0.45 MB |
| 100K | Synthetic | 0.43 MB | 0.46 MB |
| 1M | Native | 3.85 MB | 3.88 MB |
| 1M | Synthetic | 3.86 MB | 3.88 MB |
Choosing #
- Lists within the browser's element size limit, or when native scrolling behaviour
matters:
vlist. - Lists past the limit:
vlist/synthetic. - Touch-heavy lists that should move the same way on every platform:
vlist/synthetic. - Document scrolling with
page(): either entry, within the element limit.
Try both in the large list example.