vlist 3.0.1-next.1 → 3.1.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.github.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins.
4
4
 
5
- **v3.0.0** — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
5
+ **v3.1.0-next.3** (prerelease on the npm `next` tag) — Vue, Svelte, Solid and React entries in the package itself: `vlist/vue`, `vlist/svelte`, `vlist/solid` and `vlist/react`, feature-first and tree-shaken like the vanilla builder; the separate adapter packages are deprecated. With `scroll.mode` from 3.1.0-next.2: the default, `"auto"`, scrolls natively and hands a list past the browser's size limit to synthetic input. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
@@ -10,7 +10,7 @@ The virtual list library for every framework. Ultra efficient, batteries-include
10
10
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
11
11
 
12
12
  - **Accessible** — `a11y()` or a selection mode adds WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
- - **Zero dependencies** — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React
13
+ - **Zero dependencies** — framework-agnostic core, with Vue, Svelte, Solid and React entries in the same package
14
14
  - **10.3 KB gzipped** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
16
  - **Tree, grid, masonry, carousel, table, groups, data, selection, search, sortable, transition** — all opt-in
@@ -24,42 +24,121 @@ The virtual list library for every framework. Ultra efficient, batteries-include
24
24
  |---|---|---|---|---|---|
25
25
  | **A11y** | WAI-ARIA + 2D keyboard, one plugin | None (DIY) | Partial | Minimal | None |
26
26
  | **Grid + Masonry + Table** | All | Grid only | Grid + Table | Grid only | None |
27
- | **Vue** | 0.6 KB adapter | Yes | — | Yes | 11.8 KB |
28
- | **Svelte** | 0.5 KB adapter | Yes | — | Yes | — |
29
- | **Solid** | 0.5 KB adapter | Yes | — | Yes | — |
27
+ | **Vue** | `vlist/vue`, 0.4 KB | Yes | — | Yes | 11.8 KB |
28
+ | **Svelte** | `vlist/svelte`, 0.3 KB | Yes | — | Yes | — |
29
+ | **Solid** | `vlist/solid`, 0.3 KB | Yes | — | Yes | — |
30
30
  | **Vanilla JS** | Native | Yes | — | — | — |
31
31
  | **Constant memory** | ~0.1 MB at 1M | No | No | No | No |
32
32
 
33
- ## Framework Adapters
33
+ ## Frameworks
34
34
 
35
- | Framework | Package | Size |
36
- |-----------|---------|------|
37
- | Vanilla JS | `vlist` | Native — no adapter needed |
38
- | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
39
- | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
40
- | SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB gzip |
41
- | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
35
+ Vue, Svelte, Solid and React each have an entry in the `vlist` package itself. Like the vanilla builder, they are feature-first: you pass plugins from `vlist`, so an app only bundles the features it uses. Every entry imports the core from `vlist`, and none carries a copy of its own.
36
+
37
+ | Framework | Import | Entry size |
38
+ |-----------|--------|------------|
39
+ | Vue 3 | `vlist/vue` | 0.4 KB gzip |
40
+ | Svelte 4 and 5 | `vlist/svelte` | 0.3 KB gzip |
41
+ | SolidJS | `vlist/solid` | 0.3 KB gzip |
42
+ | Vanilla JS | `vlist` | — |
43
+ | React 17+ | `vlist/react` | 0.4 KB gzip |
42
44
 
43
45
  ```bash
44
- npm install vlist # vanilla JS
45
- npm install vlist@2 # the 2.x line, still maintained
46
- npm install vlist vlist-vue # or vlist-svelte / vlist-solidjs / vlist-react
46
+ npm install vlist
47
+ ```
48
+
49
+ Frameworks are optional peer dependencies, so install only the one you use.
50
+
51
+ **Vue**
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ import { useVList } from "vlist/vue";
56
+ import { grid } from "vlist";
57
+
58
+ const { containerRef, instance } = useVList(
59
+ { items, item: { height: 200, template: renderPhoto } },
60
+ [grid({ columns: 4, gap: 16 })],
61
+ );
62
+ </script>
63
+
64
+ <template><div ref="containerRef" style="height: 600px" /></template>
65
+ ```
66
+
67
+ If you pass a `ref` as the config, the list updates when its `items` change. `useVListEvent(instance, "item:click", handler)` subscribes to an event for the component's lifetime.
68
+
69
+ **Svelte**
70
+
71
+ ```svelte
72
+ <script>
73
+ import { vlist } from "vlist/svelte";
74
+ import { grid } from "vlist";
75
+ </script>
76
+
77
+ <div
78
+ style="height: 600px"
79
+ use:vlist={{
80
+ config: { items, item: { height: 200, template: renderPhoto } },
81
+ plugins: [grid({ columns: 4, gap: 16 })],
82
+ onInstance: (list) => (instance = list),
83
+ }}
84
+ />
47
85
  ```
48
86
 
49
- Adapters use native scrolling by default. With an adapter that forwards `factory`, select synthetic input explicitly:
87
+ **Solid**
88
+
89
+ ```tsx
90
+ import { createVList } from "vlist/solid";
91
+ import { grid } from "vlist";
92
+
93
+ const { setRef, instance } = createVList(
94
+ () => ({ items: items(), item: { height: 200, template: renderPhoto } }),
95
+ [grid({ columns: 4, gap: 16 })],
96
+ );
97
+
98
+ return <div ref={setRef} style={{ height: "600px" }} />;
99
+ ```
100
+
101
+ `createVList` keeps the Solid primitive idiom. In a file that also uses the core builder, alias one of the two: `import { createVList as createSolidVList } from "vlist/solid"`.
102
+
103
+ **React**
104
+
105
+ ```tsx
106
+ import { useVList, useVListEvent } from "vlist/react";
107
+ import { grid } from "vlist";
108
+
109
+ function Photos({ items }) {
110
+ const { containerRef, instanceRef } = useVList(
111
+ { items, item: { height: 200, template: renderPhoto } },
112
+ [grid({ columns: 4, gap: 16 })],
113
+ );
114
+ useVListEvent(instanceRef, "item:click", ({ item }) => open(item));
115
+ return <div ref={containerRef} style={{ height: 600 }} />;
116
+ }
117
+ ```
118
+
119
+ Plugins are read once, at mount; changing them means remounting the list (in React, with a `key`). An `items` change updates the list in place. `scroll.mode` and other structural options also take effect only at mount.
120
+
121
+ **Migrating from `vlist-vue`, `vlist-svelte`, `vlist-solidjs` or `vlist-react`.** Those packages are deprecated. Change the import path, and pass features as plugins instead of config fields:
122
+
123
+ | Deprecated package | Use |
124
+ |---|---|
125
+ | `vlist-vue` | `vlist/vue` |
126
+ | `vlist-svelte` | `vlist/svelte` |
127
+ | `vlist-solidjs` | `vlist/solid` |
128
+ | `vlist-react` | `vlist/react` |
50
129
 
51
130
  ```ts
131
+ // before
52
132
  import { useVList } from "vlist-react";
53
- import { createVList } from "vlist/synthetic";
133
+ useVList({ items, item, selection: { mode: "single" } });
54
134
 
55
- useVList({
56
- factory: createVList,
57
- items,
58
- item: { height: 48, template: item => String(item.id) },
59
- });
135
+ // after
136
+ import { useVList } from "vlist/react";
137
+ import { selection } from "vlist";
138
+ useVList({ items, item }, [selection({ mode: "single" })]);
60
139
  ```
61
140
 
62
- The same factory option is available to the other adapters. `vlist/config` defaults to native scrolling. The factory is structural configuration: changing it requires recreating the list.
141
+ Their 3.1 releases will be built on these entries and keep the config-based API, so existing code keeps working while you migrate.
63
142
 
64
143
  ## Quick Start
65
144
 
@@ -108,47 +187,54 @@ const list = createVList({
108
187
 
109
188
  ## Scroll input
110
189
 
111
- Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel and sortable work with this factory; horizontal RTL lists are rejected by both entries. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
190
+ `scroll.mode` chooses who owns scroll input, on the one `vlist` entry. Plugins come from `vlist` in every mode.
112
191
 
113
- | Entry | Input model | Use |
192
+ | `scroll.mode` | Input | Use |
114
193
  |---|---|---|
115
- | `vlist` | Native (default) | Browser scrolling and all supported plugins |
116
- | `vlist/synthetic` | Synthetic (opt-in) | Huge lists and application-owned touch motion |
117
- | `vlist/native` | Alias of `vlist` (deprecated) | Compatibility with `3.0.0-next.1`; use `vlist` for new code |
194
+ | `"auto"` (default) | Native; past the browser's element size limit (16,000,000 px of content) the list hands itself to synthetic input in place, and takes native input back below 12,000,000 px | Every list: nothing to choose, however large it grows |
195
+ | `"native"` | Always native | Browser scrolling only; past the limit the last rows are out of reach, and `content:size:overflow` says so |
196
+ | `"synthetic"` | vlist owns wheel, touch and keys from the start | Application-owned touch motion |
118
197
 
119
- ### Synthetic input
120
-
121
- Select synthetic input by importing its factory; there is no `scroll.mode` option. Plugins still come from `vlist`.
198
+ Native input keeps browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. The synthetic driver is a separate file, `synthetic-driver.js`, downloaded the first time a list needs it; until it arrives the list scrolls natively. Each handoff emits `scroll:mode`.
122
199
 
123
200
  ```typescript
124
- import { createVList } from 'vlist/synthetic'
125
- import { scrollbar } from 'vlist'
201
+ import { createVList } from 'vlist'
126
202
  import 'vlist/styles'
127
203
 
128
204
  const list = createVList({
129
205
  container: '#my-list',
130
206
  items: Array.from({ length: 1000 }, (_, id) => ({ id, name: `Row ${id}` })),
131
207
  item: { height: 48, template: item => `<div>${item.name}</div>` },
132
- }, [scrollbar()])
208
+ scroll: { mode: 'synthetic' },
209
+ })
133
210
  ```
134
211
 
135
- `page()` uses native document scrolling through an external source with either entry. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use `vlist/synthetic` viewport scrolling for larger lists. Default native viewport lists warn once when content exceeds their browser-size safety limit, pointing to that entry.
212
+ | Entry | Use |
213
+ |---|---|
214
+ | `vlist` | Every list; input chosen by `scroll.mode` |
215
+ | `vlist/synthetic` | Deprecated: `scroll: { mode: "synthetic" }`. Kept through 3.x; it bundles the driver instead of loading it |
216
+ | `vlist/native` | Deprecated alias of `vlist` |
217
+
218
+ ### Synthetic input
219
+
220
+ A synthetic list draws its own scrollbar, since the browser draws none for content the size of its viewport: the `scrollbar()` plugin's, loaded with the driver. `scroll.scrollbar` options configure it, `"none"` skips it, and a list with `scrollbar()` keeps that one. `scroll.scrollbar: "native"` asks for the browser's scrollbar, so `mode: "synthetic"` rejects it.
221
+
222
+ `page()` scrolls the document natively in every mode. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use viewport scrolling (`"auto"` or `"synthetic"`) for larger lists.
136
223
 
137
- `carousel()` works with either entry. Synthetic carousel folds preserve touch motion and smooth snapping across whole laps. `page()` and carousel remain incompatible. The shared carousel plugin includes its native wrap handler even when used with synthetic input; the combined size is reported below.
224
+ `carousel()` runs on its own wrap runway with `"auto"` and `"native"`, and on the synthetic handler with `"synthetic"`: folds preserve touch motion and smooth snapping across whole laps. The first carousel on a page starts on its runway and switches in place once the driver loads and the carousel is idle. `page()` and carousel remain incompatible.
138
225
 
139
226
  Synthetic input limitations:
140
227
 
141
- - Horizontal RTL lists throw at creation — in this entry and in `vlist`. Vertical lists and tables support `dir="rtl"` in both, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
142
- - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use the native default when boundary gestures must scroll the parent page.
143
- - There is no native main-axis scrollbar. Add `scrollbar()` for an accessible custom scrollbar. The synthetic entry rejects the `"native"` and `"none"` scrollbar strings.
228
+ - Horizontal RTL lists throw at creation, in every mode. Vertical lists and tables support `dir="rtl"`, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
229
+ - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use `"native"` when boundary gestures must scroll the parent page.
144
230
  - Inertia initializes its frame clock on the first frame after release, adding up to one frame of release latency.
145
231
  - Wheel input at an edge is left to the page when it cannot move the list. Native cross-axis scrolling remains available.
146
232
 
147
- Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model).
233
+ Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model) and [RFC-015](https://vlist.io/docs/rfcs/RFC-015-Overflow-Handoff).
148
234
 
149
235
  ### Sortable gestures
150
236
 
151
- `sortable()` works with either input entry. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
237
+ `sortable()` works in every scroll mode. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
152
238
 
153
239
  ```typescript
154
240
  sortable({ touchDelay: 350, dragThreshold: 5 })
@@ -161,46 +247,51 @@ The touch-only `.vlist-item--touch-sort` class suppresses selection and callouts
161
247
 
162
248
  ## Migrating to 3.0
163
249
 
164
- `3.0.0-next.1` used synthetic input by default. `3.0.0-next.2` restores the 2.x default. Import `vlist/synthetic` explicitly to retain synthetic behavior across these prereleases. `vlist/native` remains a deprecated compatibility alias; its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
250
+ 3.0 scrolls natively by default, as 2.x did. Since 3.1, `scroll.mode` chooses the input on the one `vlist` entry, and its default, `"auto"`, hands huge lists to synthetic input by itself. `vlist/synthetic` and `vlist/native` are deprecated aliases; `vlist/native` its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
165
251
 
166
252
  | 2.x option or API | 3.0 replacement |
167
253
  |---|---|
168
- | `scroll.mode` (all values, both entries) | Omit it. `vlist` provides native scrolling; import `vlist/synthetic` for synthetic input. |
169
- | `scroll.runway` (both entries) | Remove it. For huge lists use `vlist/synthetic`, which needs no native runway. |
170
- | `scroll.scrollbar: "native"` or `"none"` | Retained in default `ScrollConfig`. With synthetic input, omit the string and optionally install `scrollbar()`. |
254
+ | `scroll.mode` | Since 3.1: `"auto"` (default), `"native"` or `"synthetic"`. The 2.x `"bounded"` is refused. |
255
+ | `scroll.runway` | Remove it. Huge lists need nothing: the default `scroll.mode`, `"auto"`, hands them to synthetic input. |
256
+ | `scroll.scrollbar: "native"` or `"none"` | Retained. `"native"` needs native input; a synthetic list draws its own scrollbar, which `"none"` skips. |
171
257
  | `PluginContext.setScrollFns`, `disableDefaultScroll` | Use `ctx.scroll.setSource` to supply an external position source and commit callback. |
172
258
  | Flat `PluginContext` members (`registerMethod`, `forceRender`, `sizeCache`, `scrollTo`, …) | Grouped by capability: `ctx.hooks.method`, `ctx.render.force`, `ctx.sizes.cache`, `ctx.scroll.to`, `ctx.items.all`. `pool`, `config`, `emitter`, `template` and `getState()` stay at the top level. |
173
- | `scale()` and `ScalePluginConfig` | Remove them; use `vlist/synthetic` for the full logical range. Config no longer installs a scale stub. |
259
+ | `scale()` and `ScalePluginConfig` | Remove them; the default `scroll.mode` covers the full logical range. Config no longer installs a scale stub. |
174
260
 
175
- Removed mode/runway options throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers supply their handler factory as the second argument of `ctx.scroll.setWrap` (named `setBoundedWrap` in 3.0.0, still accepted).
261
+ `scroll.runway` and the bounded mode throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers supply their handler factory as the second argument of `ctx.scroll.setWrap` (named `setBoundedWrap` in 3.0.0, still accepted).
176
262
 
177
263
  ## Plugins
178
264
 
179
265
  | Entry / export | Minified | Gzipped |
180
266
  |---|---:|---:|
181
267
  | **Base (`vlist`)** | 27.3 KB | 10.3 KB |
182
- | `vlist/synthetic` | 34.2 KB | 12.9 KB |
268
+ | `vlist/synthetic` (deprecated) | 34.2 KB | 12.9 KB |
183
269
  | `vlist/native` (alias) | 27.3 KB | 10.3 KB |
184
270
  | `a11y()` | 31.2 KB | 11.7 KB |
185
- | `selection()` | 36.9 KB | 13.2 KB |
186
- | `data()` | 40.9 KB | 15.1 KB |
271
+ | `selection()` | 36.9 KB | 13.3 KB |
272
+ | `data()` | 41.0 KB | 15.1 KB |
187
273
  | `scrollbar()` | 35.5 KB | 13.2 KB |
188
274
  | `sortable()` | 39.2 KB | 13.9 KB |
189
275
  | `groups()` | 43.4 KB | 15.7 KB |
190
- | `page()` | 29.8 KB | 11.2 KB |
276
+ | `page()` | 29.9 KB | 11.2 KB |
191
277
  | `snapshots()` | 30.6 KB | 11.4 KB |
192
- | `transition()` | 34.0 KB | 12.3 KB |
193
- | `autosize()` | 30.6 KB | 11.4 KB |
278
+ | `transition()` | 34.1 KB | 12.3 KB |
279
+ | `autosize()` | 30.6 KB | 11.5 KB |
194
280
  | `grid()` | 34.7 KB | 12.9 KB |
195
281
  | `table()` | 45.8 KB | 16.2 KB |
196
282
  | `masonry()` | 39.4 KB | 14.7 KB |
197
283
  | `tree()` | 43.5 KB | 15.7 KB |
198
- | `search()` | 36.8 KB | 13.6 KB |
199
- | `carousel()` | 42.2 KB | 15.4 KB |
200
- | `vlist/synthetic` + `carousel()` | 48.3 KB | 17.7 KB |
284
+ | `search()` | 36.9 KB | 13.6 KB |
285
+ | `carousel()` | 43.2 KB | 15.8 KB |
286
+ | `vlist/synthetic` + `carousel()` | 49.3 KB | 18.1 KB |
201
287
  | `vlist/synthetic` + `sortable()` | 46.1 KB | 16.5 KB |
288
+ | `vlist/vue` `useVList` | 27.7 KB | 10.5 KB |
289
+ | `vlist/svelte` `vlist` | 27.5 KB | 10.4 KB |
290
+ | `vlist/solid` `createVList` | 27.6 KB | 10.4 KB |
291
+ | `vlist/react` `useVList` | 27.8 KB | 10.5 KB |
292
+ | `vlist/react` + `grid()` | 35.2 KB | 13.1 KB |
202
293
 
203
- Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **10,531 bytes gzipped**, within the 10.3 KB budget (10,547 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **13,172 bytes** before plugins; with `scroll.mode`, a `vlist` list downloads the driver and its scrollbar as `synthetic-driver.js` (6.2 KB gzipped) only when it goes synthetic. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
294
+ Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin; framework rows add the entry, with the framework itself external. The base is **10,539 bytes gzipped**, within the 10.3 KB budget (10,547 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **13,192 bytes** before plugins; with `scroll.mode`, a `vlist` list downloads the driver and its scrollbar as `synthetic-driver.js` (6.2 KB gzipped) only when it goes synthetic. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
204
295
 
205
296
  ## Examples
206
297
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins.
4
4
 
5
- **v3.0.0** — Native scrolling by default, opt-in `vlist/synthetic`, and removal of deprecated scroll configuration and plugin hooks. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
5
+ **v3.1.0-next.3** (prerelease on the npm `next` tag) — Vue, Svelte, Solid and React entries in the package itself: `vlist/vue`, `vlist/svelte`, `vlist/solid` and `vlist/react`, feature-first and tree-shaken like the vanilla builder; the separate adapter packages are deprecated. With `scroll.mode` from 3.1.0-next.2: the default, `"auto"`, scrolls natively and hands a list past the browser's size limit to synthetic input. See the [changelog](https://github.com/floor/vlist/blob/next/CHANGELOG.md).
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)
8
8
  [![bundle size](https://img.shields.io/bundlephobia/minzip/vlist)](https://bundlephobia.com/package/vlist)
@@ -10,7 +10,7 @@ The virtual list library for every framework. Ultra efficient, batteries-include
10
10
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
11
11
 
12
12
  - **Accessible** — `a11y()` or a selection mode adds WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
13
- - **Zero dependencies** — framework-agnostic core, tiny adapters for Vue, Svelte, Solid, React
13
+ - **Zero dependencies** — framework-agnostic core, with Vue, Svelte, Solid and React entries in the same package
14
14
  - **10.3 KB gzipped** — composable plugins with perfect tree-shaking
15
15
  - **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items
16
16
  - **Axis-neutral** — vertical and horizontal scrolling through a single code path, all plugins work in both orientations
@@ -55,47 +55,54 @@ const list = createVList({ container: '#app', items, item: { height: 200, templa
55
55
 
56
56
  ## Scroll input
57
57
 
58
- Native scrolling is the default: import `createVList` and plugins from `vlist`. This preserves browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. Carousel and sortable work with this factory; horizontal RTL lists are rejected by both entries. Carousel supplies its private wrap runway only when the plugin is imported; bounded scrolling is no longer a public mode.
58
+ `scroll.mode` chooses who owns scroll input, on the one `vlist` entry. Plugins come from `vlist` in every mode.
59
59
 
60
- | Entry | Input model | Use |
60
+ | `scroll.mode` | Input | Use |
61
61
  |---|---|---|
62
- | `vlist` | Native (default) | Browser scrolling and all supported plugins |
63
- | `vlist/synthetic` | Synthetic (opt-in) | Huge lists and application-owned touch motion |
64
- | `vlist/native` | Alias of `vlist` (deprecated) | Compatibility with `3.0.0-next.1`; use `vlist` for new code |
62
+ | `"auto"` (default) | Native; past the browser's element size limit (16,000,000 px of content) the list hands itself to synthetic input in place, and takes native input back below 12,000,000 px | Every list: nothing to choose, however large it grows |
63
+ | `"native"` | Always native | Browser scrolling only; past the limit the last rows are out of reach, and `content:size:overflow` says so |
64
+ | `"synthetic"` | vlist owns wheel, touch and keys from the start | Application-owned touch motion |
65
65
 
66
- ### Synthetic input
67
-
68
- Select synthetic input by importing its factory; there is no `scroll.mode` option. Plugins still come from `vlist`.
66
+ Native input keeps browser scrollbars, native touch momentum, boundary handoff and native assistive-technology scrolling. The synthetic driver is a separate file, `synthetic-driver.js`, downloaded the first time a list needs it; until it arrives the list scrolls natively. Each handoff emits `scroll:mode`.
69
67
 
70
68
  ```typescript
71
- import { createVList } from 'vlist/synthetic'
72
- import { scrollbar } from 'vlist'
69
+ import { createVList } from 'vlist'
73
70
  import 'vlist/styles'
74
71
 
75
72
  const list = createVList({
76
73
  container: '#my-list',
77
74
  items: Array.from({ length: 1000 }, (_, id) => ({ id, name: `Row ${id}` })),
78
75
  item: { height: 48, template: item => `<div>${item.name}</div>` },
79
- }, [scrollbar()])
76
+ scroll: { mode: 'synthetic' },
77
+ })
80
78
  ```
81
79
 
82
- `page()` uses native document scrolling through an external source with either entry. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use `vlist/synthetic` viewport scrolling for larger lists. Default native viewport lists warn once when content exceeds their browser-size safety limit, pointing to that entry.
80
+ | Entry | Use |
81
+ |---|---|
82
+ | `vlist` | Every list; input chosen by `scroll.mode` |
83
+ | `vlist/synthetic` | Deprecated: `scroll: { mode: "synthetic" }`. Kept through 3.x; it bundles the driver instead of loading it |
84
+ | `vlist/native` | Deprecated alias of `vlist` |
85
+
86
+ ### Synthetic input
87
+
88
+ A synthetic list draws its own scrollbar, since the browser draws none for content the size of its viewport: the `scrollbar()` plugin's, loaded with the driver. `scroll.scrollbar` options configure it, `"none"` skips it, and a list with `scrollbar()` keeps that one. `scroll.scrollbar: "native"` asks for the browser's scrollbar, so `mode: "synthetic"` rejects it.
83
89
 
84
- `carousel()` works with either entry. Synthetic carousel folds preserve touch motion and smooth snapping across whole laps. `page()` and carousel remain incompatible. The shared carousel plugin includes its native wrap handler even when used with synthetic input; the combined size is reported below.
90
+ `page()` scrolls the document natively in every mode. Its content must fit the 16,777,216 px document element limit; creation throws above that limit when the size is known, and later growth warns once. A deferred custom renderer whose size is first committed during rendering also warns once. Use viewport scrolling (`"auto"` or `"synthetic"`) for larger lists.
91
+
92
+ `carousel()` runs on its own wrap runway with `"auto"` and `"native"`, and on the synthetic handler with `"synthetic"`: folds preserve touch motion and smooth snapping across whole laps. The first carousel on a page starts on its runway and switches in place once the driver loads and the carousel is idle. `page()` and carousel remain incompatible.
85
93
 
86
94
  Synthetic input limitations:
87
95
 
88
- - Horizontal RTL lists throw at creation — in this entry and in `vlist`. Vertical lists and tables support `dir="rtl"` in both, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
89
- - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use the native default when boundary gestures must scroll the parent page.
90
- - There is no native main-axis scrollbar. Add `scrollbar()` for an accessible custom scrollbar. The synthetic entry rejects the `"native"` and `"none"` scrollbar strings.
96
+ - Horizontal RTL lists throw at creation, in every mode. Vertical lists and tables support `dir="rtl"`, including cross-axis wheel movement, aligned table headers and keyboard column navigation.
97
+ - Same-axis touch stops at either boundary with no parent handoff, including gestures starting inside an edge-pinned list. Use `"native"` when boundary gestures must scroll the parent page.
91
98
  - Inertia initializes its frame clock on the first frame after release, adding up to one frame of release latency.
92
99
  - Wheel input at an edge is left to the page when it cannot move the list. Native cross-axis scrolling remains available.
93
100
 
94
- Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model).
101
+ Measurement corrections from autosize preserve ongoing synthetic motion. Existing plugin conflicts still apply. See the [scroll input contract](https://vlist.io/docs/rfcs/RFC-014-Scroll-Input-Model) and [RFC-015](https://vlist.io/docs/rfcs/RFC-015-Overflow-Handoff).
95
102
 
96
103
  ### Sortable gestures
97
104
 
98
- `sortable()` works with either input entry. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
105
+ `sortable()` works in every scroll mode. Mouse and trackpad dragging still starts after `dragThreshold` (5 px by default). Touch and pen without a handle use `touchDelay`, a 350 ms long press; moving at least `dragThreshold` pixels in any direction before it completes yields to scrolling. This delay separates a deliberate hold from a quick flick. With `handle`, dragging starts at the threshold without waiting; touches elsewhere scroll.
99
106
 
100
107
  ```typescript
101
108
  sortable({ touchDelay: 350, dragThreshold: 5 })
@@ -108,69 +115,160 @@ The touch-only `.vlist-item--touch-sort` class suppresses selection and callouts
108
115
 
109
116
  ## Migrating to 3.0
110
117
 
111
- `3.0.0-next.1` used synthetic input by default. `3.0.0-next.2` restores the 2.x default. Import `vlist/synthetic` explicitly to retain synthetic behavior across these prereleases. `vlist/native` remains a deprecated compatibility alias; its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
118
+ 3.0 scrolls natively by default, as 2.x did. Since 3.1, `scroll.mode` chooses the input on the one `vlist` entry, and its default, `"auto"`, hands huge lists to synthetic input by itself. `vlist/synthetic` and `vlist/native` are deprecated aliases; `vlist/native` its `NativeScrollConfig` and `NativeCreateVListConfig` types alias the standard `ScrollConfig` and `CreateVListConfig`.
112
119
 
113
120
  | 2.x option or API | 3.0 replacement |
114
121
  |---|---|
115
- | `scroll.mode` (all values, both entries) | Omit it. `vlist` provides native scrolling; import `vlist/synthetic` for synthetic input. |
116
- | `scroll.runway` (both entries) | Remove it. For huge lists use `vlist/synthetic`, which needs no native runway. |
117
- | `scroll.scrollbar: "native"` or `"none"` | Retained in default `ScrollConfig`. With synthetic input, omit the string and optionally install `scrollbar()`. |
122
+ | `scroll.mode` | Since 3.1: `"auto"` (default), `"native"` or `"synthetic"`. The 2.x `"bounded"` is refused. |
123
+ | `scroll.runway` | Remove it. Huge lists need nothing: the default `scroll.mode`, `"auto"`, hands them to synthetic input. |
124
+ | `scroll.scrollbar: "native"` or `"none"` | Retained. `"native"` needs native input; a synthetic list draws its own scrollbar, which `"none"` skips. |
118
125
  | `PluginContext.setScrollFns`, `disableDefaultScroll` | Use `setScrollSource` to supply an external position source and commit callback. |
119
- | `scale()` and `ScalePluginConfig` | Remove them; use `vlist/synthetic` for the full logical range. Config no longer installs a scale stub. |
126
+ | `scale()` and `ScalePluginConfig` | Remove them; the default `scroll.mode` covers the full logical range. Config no longer installs a scale stub. |
120
127
 
121
- Removed mode/runway options throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers supply their handler factory as the second argument of `ctx.scroll.setWrap` (named `setBoundedWrap` in 3.0.0, still accepted).
128
+ `scroll.runway` and the bounded mode throw a migration error before creating DOM. `vlist/config` wires only what the config asks for: `selection`, the custom scrollbar, `snapshots` and `a11y` are each opt-in, so an adapter list matches a core list built from the same options. It keeps the scrollbar options convenience and its top-level `scrollbar: "none"`; pass `scrollbar: true` for the overlay scrollbar earlier versions added to every list. `baseOffset` stays private engine state; plugins use `ctx.scroll.getRenderOrigin()`. Custom wrap providers supply their handler factory as the second argument of `ctx.scroll.setWrap` (named `setBoundedWrap` in 3.0.0, still accepted).
122
129
 
123
130
  ## Plugins
124
131
 
125
132
  | Entry / export | Minified | Gzipped |
126
133
  |---|---:|---:|
127
134
  | **Base (`vlist`)** | 27.3 KB | 10.3 KB |
128
- | `vlist/synthetic` | 34.2 KB | 12.9 KB |
135
+ | `vlist/synthetic` (deprecated) | 34.2 KB | 12.9 KB |
129
136
  | `vlist/native` (alias) | 27.3 KB | 10.3 KB |
130
137
  | `a11y()` | 31.2 KB | 11.7 KB |
131
- | `selection()` | 36.9 KB | 13.2 KB |
132
- | `data()` | 40.9 KB | 15.1 KB |
138
+ | `selection()` | 36.9 KB | 13.3 KB |
139
+ | `data()` | 41.0 KB | 15.1 KB |
133
140
  | `scrollbar()` | 35.5 KB | 13.2 KB |
134
141
  | `sortable()` | 39.2 KB | 13.9 KB |
135
142
  | `groups()` | 43.4 KB | 15.7 KB |
136
- | `page()` | 29.8 KB | 11.2 KB |
143
+ | `page()` | 29.9 KB | 11.2 KB |
137
144
  | `snapshots()` | 30.6 KB | 11.4 KB |
138
- | `transition()` | 34.0 KB | 12.3 KB |
139
- | `autosize()` | 30.6 KB | 11.4 KB |
145
+ | `transition()` | 34.1 KB | 12.3 KB |
146
+ | `autosize()` | 30.6 KB | 11.5 KB |
140
147
  | `grid()` | 34.7 KB | 12.9 KB |
141
148
  | `table()` | 45.8 KB | 16.2 KB |
142
149
  | `masonry()` | 39.4 KB | 14.7 KB |
143
150
  | `tree()` | 43.5 KB | 15.7 KB |
144
- | `search()` | 36.8 KB | 13.6 KB |
145
- | `carousel()` | 42.2 KB | 15.4 KB |
146
- | `vlist/synthetic` + `carousel()` | 48.3 KB | 17.7 KB |
151
+ | `search()` | 36.9 KB | 13.6 KB |
152
+ | `carousel()` | 43.2 KB | 15.8 KB |
153
+ | `vlist/synthetic` + `carousel()` | 49.3 KB | 18.1 KB |
147
154
  | `vlist/synthetic` + `sortable()` | 46.1 KB | 16.5 KB |
155
+ | `vlist/vue` `useVList` | 27.7 KB | 10.5 KB |
156
+ | `vlist/svelte` `vlist` | 27.5 KB | 10.4 KB |
157
+ | `vlist/solid` `createVList` | 27.6 KB | 10.4 KB |
158
+ | `vlist/react` `useVList` | 27.8 KB | 10.5 KB |
159
+ | `vlist/react` + `grid()` | 35.2 KB | 13.1 KB |
160
+
161
+ Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin; framework rows add the entry, with the framework itself external. The base is **10,539 bytes gzipped**, within the 10.3 KB budget (10,547 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **13,192 bytes** before plugins; with `scroll.mode`, a `vlist` list downloads the driver and its scrollbar as `synthetic-driver.js` (6.2 KB gzipped) only when it goes synthetic. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
162
+
163
+ ## Frameworks
164
+
165
+ Vue, Svelte, Solid and React each have an entry in the `vlist` package itself. Like the vanilla builder, they are feature-first: you pass plugins from `vlist`, so an app only bundles the features it uses. Every entry imports the core from `vlist`, and none carries a copy of its own.
166
+
167
+ | Framework | Import | Entry size |
168
+ |-----------|--------|------------|
169
+ | Vue 3 | `vlist/vue` | 0.4 KB gzip |
170
+ | Svelte 4 and 5 | `vlist/svelte` | 0.3 KB gzip |
171
+ | SolidJS | `vlist/solid` | 0.3 KB gzip |
172
+ | Vanilla JS | `vlist` | — |
173
+ | React 17+ | `vlist/react` | 0.4 KB gzip |
174
+
175
+ ```bash
176
+ npm install vlist
177
+ ```
178
+
179
+ Frameworks are optional peer dependencies, so install only the one you use.
180
+
181
+ **Vue**
148
182
 
149
- Sizes are tree-shaken totals from `bun run size`, not additive plugin costs. Plugin rows include the native default factory plus that plugin. The base is **10,531 bytes gzipped**, within the 10.3 KB budget (10,547 bytes). `bun run size` fails if a scenario fails to build, an unused plugin leaks into a bundle, or any published size exceeds its gzip budget. Synthetic input is **13,172 bytes** before plugins; with `scroll.mode`, a `vlist` list downloads the driver and its scrollbar as `synthetic-driver.js` (6.2 KB gzipped) only when it goes synthetic. The alias row measures the same source factory; the distributed alias re-exports it without duplicating the implementation.
183
+ ```vue
184
+ <script setup lang="ts">
185
+ import { useVList } from "vlist/vue";
186
+ import { grid } from "vlist";
150
187
 
151
- ## Framework Adapters
188
+ const { containerRef, instance } = useVList(
189
+ { items, item: { height: 200, template: renderPhoto } },
190
+ [grid({ columns: 4, gap: 16 })],
191
+ );
192
+ </script>
152
193
 
153
- | Framework | Package | Size |
154
- |-----------|---------|------|
155
- | Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB |
156
- | Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB |
157
- | SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB |
158
- | React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB |
194
+ <template><div ref="containerRef" style="height: 600px" /></template>
195
+ ```
196
+
197
+ If you pass a `ref` as the config, the list updates when its `items` change. `useVListEvent(instance, "item:click", handler)` subscribes to an event for the component's lifetime.
198
+
199
+ **Svelte**
200
+
201
+ ```svelte
202
+ <script>
203
+ import { vlist } from "vlist/svelte";
204
+ import { grid } from "vlist";
205
+ </script>
206
+
207
+ <div
208
+ style="height: 600px"
209
+ use:vlist={{
210
+ config: { items, item: { height: 200, template: renderPhoto } },
211
+ plugins: [grid({ columns: 4, gap: 16 })],
212
+ onInstance: (list) => (instance = list),
213
+ }}
214
+ />
215
+ ```
216
+
217
+ **Solid**
218
+
219
+ ```tsx
220
+ import { createVList } from "vlist/solid";
221
+ import { grid } from "vlist";
159
222
 
160
- Adapters use native scrolling by default. With an adapter that forwards `factory`, select synthetic input explicitly:
223
+ const { setRef, instance } = createVList(
224
+ () => ({ items: items(), item: { height: 200, template: renderPhoto } }),
225
+ [grid({ columns: 4, gap: 16 })],
226
+ );
227
+
228
+ return <div ref={setRef} style={{ height: "600px" }} />;
229
+ ```
230
+
231
+ `createVList` keeps the Solid primitive idiom. In a file that also uses the core builder, alias one of the two: `import { createVList as createSolidVList } from "vlist/solid"`.
232
+
233
+ **React**
234
+
235
+ ```tsx
236
+ import { useVList, useVListEvent } from "vlist/react";
237
+ import { grid } from "vlist";
238
+
239
+ function Photos({ items }) {
240
+ const { containerRef, instanceRef } = useVList(
241
+ { items, item: { height: 200, template: renderPhoto } },
242
+ [grid({ columns: 4, gap: 16 })],
243
+ );
244
+ useVListEvent(instanceRef, "item:click", ({ item }) => open(item));
245
+ return <div ref={containerRef} style={{ height: 600 }} />;
246
+ }
247
+ ```
248
+
249
+ Plugins are read once, at mount; changing them means remounting the list (in React, with a `key`). An `items` change updates the list in place. `scroll.mode` and other structural options also take effect only at mount.
250
+
251
+ **Migrating from `vlist-vue`, `vlist-svelte`, `vlist-solidjs` or `vlist-react`.** Those packages are deprecated. Change the import path, and pass features as plugins instead of config fields:
252
+
253
+ | Deprecated package | Use |
254
+ |---|---|
255
+ | `vlist-vue` | `vlist/vue` |
256
+ | `vlist-svelte` | `vlist/svelte` |
257
+ | `vlist-solidjs` | `vlist/solid` |
258
+ | `vlist-react` | `vlist/react` |
161
259
 
162
260
  ```ts
261
+ // before
163
262
  import { useVList } from "vlist-react";
164
- import { createVList } from "vlist/synthetic";
263
+ useVList({ items, item, selection: { mode: "single" } });
165
264
 
166
- useVList({
167
- factory: createVList,
168
- items,
169
- item: { height: 48, template: item => String(item.id) },
170
- });
265
+ // after
266
+ import { useVList } from "vlist/react";
267
+ import { selection } from "vlist";
268
+ useVList({ items, item }, [selection({ mode: "single" })]);
171
269
  ```
172
270
 
173
- The same factory option is available to the other adapters. `vlist/config` defaults to native scrolling. The factory is structural configuration: changing it requires recreating the list.
271
+ Their 3.1 releases will be built on these entries and keep the config-based API, so existing code keeps working while you migrate.
174
272
 
175
273
  ## Docs & Examples
176
274
 
package/dist/config.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * config (the adapters) pull in this module and, with it, every plugin it wires.
17
17
  */
18
18
  import type { VListItem, GroupsConfig, VListAdapter, ScrollConfig } from "./types";
19
- import { createVList } from "./core/create";
19
+ import { createVList } from "./index";
20
20
  import type { CreateVListConfig, VList, VListPlugin, PluginMethods } from "./core/types";
21
21
  import type { AutosizeMethods } from "./plugins/autosize/plugin";
22
22
  import type { DataMethods } from "./plugins/data/plugin";