@floor/vlist 0.5.1
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/LICENSE +21 -0
- package/README.md +839 -0
- package/dist/adapters/index.d.ts +20 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/react.d.ts +119 -0
- package/dist/adapters/react.d.ts.map +1 -0
- package/dist/adapters/svelte.d.ts +198 -0
- package/dist/adapters/svelte.d.ts.map +1 -0
- package/dist/adapters/vue.d.ts +151 -0
- package/dist/adapters/vue.d.ts.map +1 -0
- package/dist/builder/context.d.ts +36 -0
- package/dist/builder/context.d.ts.map +1 -0
- package/dist/builder/core.d.ts +16 -0
- package/dist/builder/core.d.ts.map +1 -0
- package/dist/builder/data.d.ts +71 -0
- package/dist/builder/data.d.ts.map +1 -0
- package/dist/builder/index.d.ts +25 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +1 -0
- package/dist/builder/types.d.ts +269 -0
- package/dist/builder/types.d.ts.map +1 -0
- package/dist/compression/index.js +1 -0
- package/dist/constants.d.ts +65 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core-light.d.ts +104 -0
- package/dist/core-light.d.ts.map +1 -0
- package/dist/core-light.js +1 -0
- package/dist/core.d.ts +129 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/data/index.js +1 -0
- package/dist/events/emitter.d.ts +20 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/grid/index.js +1 -0
- package/dist/groups/index.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/compression/index.d.ts +10 -0
- package/dist/plugins/compression/index.d.ts.map +1 -0
- package/dist/plugins/compression/plugin.d.ts +42 -0
- package/dist/plugins/compression/plugin.d.ts.map +1 -0
- package/dist/plugins/data/index.d.ts +9 -0
- package/dist/plugins/data/index.d.ts.map +1 -0
- package/dist/plugins/data/manager.d.ts +103 -0
- package/dist/plugins/data/manager.d.ts.map +1 -0
- package/dist/plugins/data/placeholder.d.ts +62 -0
- package/dist/plugins/data/placeholder.d.ts.map +1 -0
- package/dist/plugins/data/plugin.d.ts +60 -0
- package/dist/plugins/data/plugin.d.ts.map +1 -0
- package/dist/plugins/data/sparse.d.ts +91 -0
- package/dist/plugins/data/sparse.d.ts.map +1 -0
- package/dist/plugins/grid/index.d.ts +9 -0
- package/dist/plugins/grid/index.d.ts.map +1 -0
- package/dist/plugins/grid/layout.d.ts +29 -0
- package/dist/plugins/grid/layout.d.ts.map +1 -0
- package/dist/plugins/grid/plugin.d.ts +48 -0
- package/dist/plugins/grid/plugin.d.ts.map +1 -0
- package/dist/plugins/grid/renderer.d.ts +55 -0
- package/dist/plugins/grid/renderer.d.ts.map +1 -0
- package/dist/plugins/grid/types.d.ts +71 -0
- package/dist/plugins/grid/types.d.ts.map +1 -0
- package/dist/plugins/groups/index.d.ts +10 -0
- package/dist/plugins/groups/index.d.ts.map +1 -0
- package/dist/plugins/groups/layout.d.ts +46 -0
- package/dist/plugins/groups/layout.d.ts.map +1 -0
- package/dist/plugins/groups/plugin.d.ts +63 -0
- package/dist/plugins/groups/plugin.d.ts.map +1 -0
- package/dist/plugins/groups/sticky.d.ts +33 -0
- package/dist/plugins/groups/sticky.d.ts.map +1 -0
- package/dist/plugins/groups/types.d.ts +86 -0
- package/dist/plugins/groups/types.d.ts.map +1 -0
- package/dist/plugins/scroll/controller.d.ts +121 -0
- package/dist/plugins/scroll/controller.d.ts.map +1 -0
- package/dist/plugins/scroll/index.d.ts +8 -0
- package/dist/plugins/scroll/index.d.ts.map +1 -0
- package/dist/plugins/scroll/plugin.d.ts +60 -0
- package/dist/plugins/scroll/plugin.d.ts.map +1 -0
- package/dist/plugins/scroll/scrollbar.d.ts +73 -0
- package/dist/plugins/scroll/scrollbar.d.ts.map +1 -0
- package/dist/plugins/selection/index.d.ts +7 -0
- package/dist/plugins/selection/index.d.ts.map +1 -0
- package/dist/plugins/selection/plugin.d.ts +44 -0
- package/dist/plugins/selection/plugin.d.ts.map +1 -0
- package/dist/plugins/selection/state.d.ts +102 -0
- package/dist/plugins/selection/state.d.ts.map +1 -0
- package/dist/plugins/snapshots/index.d.ts +8 -0
- package/dist/plugins/snapshots/index.d.ts.map +1 -0
- package/dist/plugins/snapshots/plugin.d.ts +44 -0
- package/dist/plugins/snapshots/plugin.d.ts.map +1 -0
- package/dist/plugins/window/index.d.ts +8 -0
- package/dist/plugins/window/index.d.ts.map +1 -0
- package/dist/plugins/window/plugin.d.ts +53 -0
- package/dist/plugins/window/plugin.d.ts.map +1 -0
- package/dist/react/index.js +1 -0
- package/dist/render/compression.d.ts +116 -0
- package/dist/render/compression.d.ts.map +1 -0
- package/dist/render/heights.d.ts +63 -0
- package/dist/render/heights.d.ts.map +1 -0
- package/dist/render/index.d.ts +9 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/renderer.d.ts +103 -0
- package/dist/render/renderer.d.ts.map +1 -0
- package/dist/render/virtual.d.ts +139 -0
- package/dist/render/virtual.d.ts.map +1 -0
- package/dist/scroll/index.js +1 -0
- package/dist/selection/index.js +1 -0
- package/dist/snapshots/index.js +1 -0
- package/dist/svelte/index.js +1 -0
- package/dist/types.d.ts +559 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vlist-extras.css +1 -0
- package/dist/vlist.css +1 -0
- package/dist/vlist.d.ts +22 -0
- package/dist/vlist.d.ts.map +1 -0
- package/dist/vue/index.js +1 -0
- package/dist/window/index.js +1 -0
- package/package.json +137 -0
package/README.md
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
# vlist
|
|
2
|
+
|
|
3
|
+
Lightweight, high-performance virtual list with zero dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vlist)
|
|
6
|
+
[](https://bundlephobia.com/package/vlist)
|
|
7
|
+
[](https://github.com/floor/vlist/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🪶 **Zero dependencies** - No external libraries required
|
|
12
|
+
- ⚡ **Blazing fast** - Only renders visible items with element pooling
|
|
13
|
+
- 🎯 **Simple API** - Easy to use with TypeScript support
|
|
14
|
+
- 📐 **Grid layout** - 2D virtualized grid with configurable columns and gap
|
|
15
|
+
- 📏 **Variable heights** - Fixed or per-item height via `(index) => number`
|
|
16
|
+
- 📜 **Infinite scroll** - Built-in async adapter support
|
|
17
|
+
- ✅ **Selection** - Single and multiple selection modes
|
|
18
|
+
- 📌 **Sticky headers** - Grouped lists with sticky section headers
|
|
19
|
+
- 🪟 **Window scrolling** - Document-level scrolling with `scrollElement: window`
|
|
20
|
+
- 🎨 **Customizable** - Beautiful, customizable styles
|
|
21
|
+
- ♿ **Accessible** - WAI-ARIA listbox pattern, `aria-setsize`/`aria-posinset`, `aria-activedescendant`, live region, keyboard navigation
|
|
22
|
+
- 🌊 **Smooth scrolling** - Animated `scrollToIndex` / `scrollToItem`
|
|
23
|
+
- 💾 **Scroll save/restore** - `getScrollSnapshot()` / `restoreScroll()` for SPA navigation
|
|
24
|
+
- 💬 **Reverse mode** - Chat UI support with auto-scroll, scroll-preserving prepend
|
|
25
|
+
- 🔌 **Framework adapters** - Thin wrappers for React, Vue, and Svelte (<1 KB each)
|
|
26
|
+
- 🌲 **Tree-shakeable** - Sub-module imports for smaller bundles
|
|
27
|
+
|
|
28
|
+
## Sandbox & Documentation
|
|
29
|
+
|
|
30
|
+
Interactive examples and documentation are available at **[vlist.dev](https://vlist.dev)**.
|
|
31
|
+
|
|
32
|
+
| Example | Description |
|
|
33
|
+
|---------|-------------|
|
|
34
|
+
| [Basic](https://vlist.dev/sandbox/basic/) | Pure vanilla JS — no frameworks, no dependencies |
|
|
35
|
+
| [Core](https://vlist.dev/sandbox/core/) | Lightweight `vlist/core` — 7.3 KB, 83% smaller |
|
|
36
|
+
| [Grid](https://vlist.dev/sandbox/grid/) | 2D photo gallery with real photos from Lorem Picsum |
|
|
37
|
+
| [Variable Heights](https://vlist.dev/sandbox/variable-heights/) | Chat-style messages with 4 different item heights |
|
|
38
|
+
| [Reverse Chat](https://vlist.dev/sandbox/reverse-chat/) | Chat UI with reverse mode, prepend history, auto-scroll |
|
|
39
|
+
| [Selection](https://vlist.dev/sandbox/selection/) | Single/multiple selection with keyboard navigation |
|
|
40
|
+
| [Infinite Scroll](https://vlist.dev/sandbox/infinite-scroll/) | Async data loading with simulated API |
|
|
41
|
+
| [Million Items](https://vlist.dev/sandbox/million-items/) | Stress test with 1–5 million items |
|
|
42
|
+
| [Velocity Loading](https://vlist.dev/sandbox/velocity-loading/) | Velocity-based load skipping demo |
|
|
43
|
+
| [Sticky Headers](https://vlist.dev/sandbox/sticky-headers/) | Grouped contact list with sticky section headers |
|
|
44
|
+
| [Window Scroll](https://vlist.dev/sandbox/window-scroll/) | Document-level scrolling with `scrollElement: window` |
|
|
45
|
+
| [Scroll Restore](https://vlist.dev/sandbox/scroll-restore/) | Save/restore scroll position across SPA navigation |
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install vlist
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Sub-module Imports
|
|
54
|
+
|
|
55
|
+
For smaller bundles, import only what you need:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createVList } from 'vlist' // full library (48.2 KB / 16.0 KB gzip)
|
|
59
|
+
import { createVList } from 'vlist/core' // lightweight core (7.8 KB / 3.2 KB gzip)
|
|
60
|
+
import { createGridLayout } from 'vlist/grid' // grid layout utilities only
|
|
61
|
+
import { createSparseStorage } from 'vlist/data' // data utilities only
|
|
62
|
+
import { getCompressionInfo } from 'vlist/compression' // compression utilities only
|
|
63
|
+
import { createSelectionState } from 'vlist/selection' // selection utilities only
|
|
64
|
+
import { createScrollController } from 'vlist/scroll' // scroll utilities only
|
|
65
|
+
import { createGroupLayout } from 'vlist/groups' // group/sticky header utilities only
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Import | Minified | Gzipped | Description |
|
|
69
|
+
|--------|----------|---------|-------------|
|
|
70
|
+
| `vlist` | 48.2 KB | 16.0 KB | All features |
|
|
71
|
+
| **`vlist/core`** | **7.8 KB** | **3.2 KB** | **Lightweight — 83% smaller** |
|
|
72
|
+
| `vlist/data` | 9.2 KB | 3.8 KB | Sparse storage, placeholders, data manager |
|
|
73
|
+
| `vlist/scroll` | 6.0 KB | 2.3 KB | Scroll controller + custom scrollbar |
|
|
74
|
+
| `vlist/grid` | 4.1 KB | 1.9 KB | Grid layout + 2D renderer |
|
|
75
|
+
| `vlist/groups` | 3.6 KB | 1.4 KB | Group layout + sticky headers |
|
|
76
|
+
| `vlist/compression` | 2.6 KB | 1.1 KB | Large-list compression utilities |
|
|
77
|
+
| `vlist/selection` | 1.9 KB | 0.7 KB | Selection state management |
|
|
78
|
+
|
|
79
|
+
### Framework Adapters
|
|
80
|
+
|
|
81
|
+
Thin wrappers for React, Vue, and Svelte — each under 1 KB:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { useVList } from 'vlist/react' // React hook (0.7 KB / 0.4 KB gzip)
|
|
85
|
+
import { useVList } from 'vlist/vue' // Vue 3 composable (0.5 KB / 0.4 KB gzip)
|
|
86
|
+
import { vlist } from 'vlist/svelte' // Svelte action (0.3 KB / 0.2 KB gzip)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Import | Minified | Gzipped | Description |
|
|
90
|
+
|--------|----------|---------|-------------|
|
|
91
|
+
| `vlist/react` | 0.7 KB | 0.4 KB | `useVList` hook + `useVListEvent` |
|
|
92
|
+
| `vlist/vue` | 0.5 KB | 0.4 KB | `useVList` composable + `useVListEvent` |
|
|
93
|
+
| `vlist/svelte` | 0.3 KB | 0.2 KB | `vlist` action + `onVListEvent` |
|
|
94
|
+
|
|
95
|
+
Adapters manage the vlist lifecycle (create on mount, destroy on unmount) and sync items reactively. See [Framework Adapters](#framework-adapters) for full examples.
|
|
96
|
+
|
|
97
|
+
## Quick Start
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { createVList } from 'vlist';
|
|
101
|
+
import 'vlist/styles';
|
|
102
|
+
|
|
103
|
+
const list = createVList({
|
|
104
|
+
container: '#my-list',
|
|
105
|
+
ariaLabel: 'Contact list',
|
|
106
|
+
item: {
|
|
107
|
+
height: 48,
|
|
108
|
+
template: (item) => `
|
|
109
|
+
<div class="item-content">
|
|
110
|
+
<img src="${item.avatar}" class="avatar" />
|
|
111
|
+
<span>${item.name}</span>
|
|
112
|
+
</div>
|
|
113
|
+
`,
|
|
114
|
+
},
|
|
115
|
+
items: [
|
|
116
|
+
{ id: 1, name: 'Alice', avatar: '/avatars/alice.jpg' },
|
|
117
|
+
{ id: 2, name: 'Bob', avatar: '/avatars/bob.jpg' },
|
|
118
|
+
// ... more items
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Lightweight Core (7.3 KB)
|
|
124
|
+
|
|
125
|
+
If you don't need selection, groups, grid, compression, custom scrollbar, or async data adapters, use the lightweight core for an **83% smaller bundle**:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { createVList } from 'vlist/core';
|
|
129
|
+
import 'vlist/styles';
|
|
130
|
+
|
|
131
|
+
const list = createVList({
|
|
132
|
+
container: '#my-list',
|
|
133
|
+
item: {
|
|
134
|
+
height: 48,
|
|
135
|
+
template: (item) => `<div>${item.name}</div>`,
|
|
136
|
+
},
|
|
137
|
+
items: myItems,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Same core API: setItems, appendItems, scrollToIndex, events, etc.
|
|
141
|
+
list.on('item:click', ({ item }) => console.log(item));
|
|
142
|
+
list.scrollToIndex(50, { behavior: 'smooth' });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The core entry supports fixed/variable heights, smooth `scrollToIndex`, all data methods (`setItems`, `appendItems`, `prependItems`, `updateItem`, `removeItem`), events, window scrolling, and ResizeObserver — everything you need for most use cases.
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
interface VListConfig<T> {
|
|
151
|
+
// Required
|
|
152
|
+
container: HTMLElement | string; // Container element or selector
|
|
153
|
+
item: {
|
|
154
|
+
height: number | ((index: number) => number); // Fixed or variable height
|
|
155
|
+
template: ItemTemplate<T>; // Render function for each item
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Layout
|
|
159
|
+
layout?: 'list' | 'grid'; // Layout mode (default: 'list')
|
|
160
|
+
grid?: { // Grid config (required when layout: 'grid')
|
|
161
|
+
columns: number; // Number of columns
|
|
162
|
+
gap?: number; // Gap between items in px (default: 0)
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Data
|
|
166
|
+
items?: T[]; // Static items array
|
|
167
|
+
adapter?: VListAdapter<T>; // Async data adapter
|
|
168
|
+
|
|
169
|
+
// Scrolling
|
|
170
|
+
overscan?: number; // Extra items to render (default: 3)
|
|
171
|
+
scroll?: {
|
|
172
|
+
wheel?: boolean; // Enable mouse wheel (default: true)
|
|
173
|
+
wrap?: boolean; // Wrap around at boundaries (default: false)
|
|
174
|
+
scrollbar?: 'native' | 'none' // Scrollbar mode (default: custom)
|
|
175
|
+
| ScrollbarOptions; // or { autoHide, autoHideDelay, minThumbSize }
|
|
176
|
+
element?: Window; // Window scrolling mode
|
|
177
|
+
idleTimeout?: number; // Scroll idle detection in ms (default: 150)
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Features
|
|
181
|
+
selection?: SelectionConfig; // Selection configuration
|
|
182
|
+
groups?: GroupsConfig; // Sticky headers / grouped lists
|
|
183
|
+
loading?: LoadingConfig; // Velocity-based loading thresholds
|
|
184
|
+
|
|
185
|
+
// Chat UI
|
|
186
|
+
reverse?: boolean; // Reverse mode (start at bottom, auto-scroll)
|
|
187
|
+
|
|
188
|
+
// Appearance
|
|
189
|
+
classPrefix?: string; // CSS class prefix (default: 'vlist')
|
|
190
|
+
ariaLabel?: string; // Accessible label for the listbox
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Examples
|
|
195
|
+
|
|
196
|
+
### Grid Layout
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const grid = createVList({
|
|
200
|
+
container: '#gallery',
|
|
201
|
+
layout: 'grid',
|
|
202
|
+
grid: {
|
|
203
|
+
columns: 4,
|
|
204
|
+
gap: 8, // 8px gap between columns AND rows
|
|
205
|
+
},
|
|
206
|
+
item: {
|
|
207
|
+
height: 200,
|
|
208
|
+
template: (item) => `
|
|
209
|
+
<div class="card">
|
|
210
|
+
<img src="${item.thumbnail}" />
|
|
211
|
+
<span>${item.title}</span>
|
|
212
|
+
</div>
|
|
213
|
+
`,
|
|
214
|
+
},
|
|
215
|
+
items: photos,
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Grid mode virtualizes by **rows** — only visible rows are in the DOM. Each item is positioned with `translate(x, y)` for GPU-accelerated rendering. Compression applies to row count, not item count.
|
|
220
|
+
|
|
221
|
+
### Variable Heights
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const list = createVList({
|
|
225
|
+
container: '#messages',
|
|
226
|
+
item: {
|
|
227
|
+
height: (index) => messages[index].type === 'header' ? 32 : 64,
|
|
228
|
+
template: (item) => `<div class="message">${item.text}</div>`,
|
|
229
|
+
},
|
|
230
|
+
items: messages,
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search for index-at-offset.
|
|
235
|
+
|
|
236
|
+
### Sticky Headers
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const list = createVList({
|
|
240
|
+
container: '#contacts',
|
|
241
|
+
item: {
|
|
242
|
+
height: 56,
|
|
243
|
+
template: (item) => `<div>${item.name}</div>`,
|
|
244
|
+
},
|
|
245
|
+
items: contacts, // Must be pre-sorted by group
|
|
246
|
+
groups: {
|
|
247
|
+
getGroupForIndex: (index) => contacts[index].lastName[0],
|
|
248
|
+
headerHeight: 36,
|
|
249
|
+
headerTemplate: (group) => `<div class="section-header">${group}</div>`,
|
|
250
|
+
sticky: true, // Headers stick to the top (default: true)
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Window Scrolling
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const list = createVList({
|
|
259
|
+
container: '#my-list',
|
|
260
|
+
scroll: { element: window }, // Use the browser's native scrollbar
|
|
261
|
+
item: {
|
|
262
|
+
height: 48,
|
|
263
|
+
template: (item) => `<div>${item.name}</div>`,
|
|
264
|
+
},
|
|
265
|
+
items: myItems,
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Wizard / Carousel (Wrap Navigation)
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const wizard = createVList({
|
|
273
|
+
container: '#wizard',
|
|
274
|
+
scroll: { wheel: false, scrollbar: 'none', wrap: true },
|
|
275
|
+
item: {
|
|
276
|
+
height: 400,
|
|
277
|
+
template: (step) => `<div class="step">${step.content}</div>`,
|
|
278
|
+
},
|
|
279
|
+
items: steps,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
let current = 0;
|
|
283
|
+
|
|
284
|
+
// No boundary checks needed — wrap handles it
|
|
285
|
+
btnNext.addEventListener('click', () => {
|
|
286
|
+
current++;
|
|
287
|
+
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
btnPrev.addEventListener('click', () => {
|
|
291
|
+
current--;
|
|
292
|
+
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Reverse Mode (Chat UI)
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const chat = createVList({
|
|
300
|
+
container: '#messages',
|
|
301
|
+
reverse: true,
|
|
302
|
+
item: {
|
|
303
|
+
height: (index) => messages[index].type === 'image' ? 200 : 60,
|
|
304
|
+
template: (msg) => `
|
|
305
|
+
<div class="bubble bubble--${msg.sender}">
|
|
306
|
+
<span class="sender">${msg.sender}</span>
|
|
307
|
+
<p>${msg.text}</p>
|
|
308
|
+
</div>
|
|
309
|
+
`,
|
|
310
|
+
},
|
|
311
|
+
items: messages, // Chronological order (oldest first)
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// New message arrives — auto-scrolls to bottom if user was at bottom
|
|
315
|
+
chat.appendItems([newMessage]);
|
|
316
|
+
|
|
317
|
+
// Load older messages — scroll position preserved (no jump)
|
|
318
|
+
chat.prependItems(olderMessages);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Reverse mode starts scrolled to the bottom. `appendItems` auto-scrolls to show new messages when the user is at the bottom. `prependItems` adjusts the scroll position so older messages appear above without disrupting the current view. Works with both fixed and variable heights. Cannot be combined with `groups` or `grid`.
|
|
322
|
+
|
|
323
|
+
### With Selection
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const list = createVList({
|
|
327
|
+
container: '#my-list',
|
|
328
|
+
item: {
|
|
329
|
+
height: 56,
|
|
330
|
+
template: (item, index, { selected }) => `
|
|
331
|
+
<div class="item-content ${selected ? 'selected' : ''}">
|
|
332
|
+
<span>${item.name}</span>
|
|
333
|
+
${selected ? '✓' : ''}
|
|
334
|
+
</div>
|
|
335
|
+
`,
|
|
336
|
+
},
|
|
337
|
+
items: users,
|
|
338
|
+
selection: {
|
|
339
|
+
mode: 'multiple', // 'none' | 'single' | 'multiple'
|
|
340
|
+
initial: [1, 2], // Initially selected IDs
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Listen for selection changes
|
|
345
|
+
list.on('selection:change', ({ selected, items }) => {
|
|
346
|
+
console.log('Selected:', selected);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Programmatic selection
|
|
350
|
+
list.select(5);
|
|
351
|
+
list.deselect(1);
|
|
352
|
+
list.selectAll();
|
|
353
|
+
list.clearSelection();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### With Infinite Scroll
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const list = createVList({
|
|
360
|
+
container: '#my-list',
|
|
361
|
+
item: {
|
|
362
|
+
height: 64,
|
|
363
|
+
template: (item) => `<div>${item.title}</div>`,
|
|
364
|
+
},
|
|
365
|
+
adapter: {
|
|
366
|
+
read: async ({ offset, limit }) => {
|
|
367
|
+
const response = await fetch(
|
|
368
|
+
`/api/items?offset=${offset}&limit=${limit}`
|
|
369
|
+
);
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
items: data.items,
|
|
374
|
+
total: data.total,
|
|
375
|
+
hasMore: data.hasMore,
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Listen for loading events
|
|
382
|
+
list.on('load:start', ({ offset, limit }) => {
|
|
383
|
+
console.log('Loading...', offset, limit);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
list.on('load:end', ({ items, total }) => {
|
|
387
|
+
console.log('Loaded', items.length, 'of', total);
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Scroll Save/Restore
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const list = createVList({
|
|
395
|
+
container: '#my-list',
|
|
396
|
+
item: {
|
|
397
|
+
height: 64,
|
|
398
|
+
template: (item) => `<div>${item.name}</div>`,
|
|
399
|
+
},
|
|
400
|
+
items: myItems,
|
|
401
|
+
selection: { mode: 'multiple' },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Save — e.g. before navigating away
|
|
405
|
+
const snapshot = list.getScrollSnapshot();
|
|
406
|
+
// { index: 523, offsetInItem: 12, selectedIds: [3, 7, 42] }
|
|
407
|
+
sessionStorage.setItem('list-scroll', JSON.stringify(snapshot));
|
|
408
|
+
|
|
409
|
+
// Restore — e.g. after navigating back and recreating the list
|
|
410
|
+
const saved = JSON.parse(sessionStorage.getItem('list-scroll'));
|
|
411
|
+
list.restoreScroll(saved);
|
|
412
|
+
// Scroll position AND selection are perfectly restored
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Framework Adapters
|
|
416
|
+
|
|
417
|
+
vlist ships thin framework wrappers that handle lifecycle and reactive item syncing. The adapters are **mount-based** — vlist manages the DOM while the framework provides the container element.
|
|
418
|
+
|
|
419
|
+
#### React
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
import { useVList, useVListEvent } from 'vlist/react';
|
|
423
|
+
|
|
424
|
+
function UserList({ users }) {
|
|
425
|
+
const { containerRef, instanceRef } = useVList({
|
|
426
|
+
item: {
|
|
427
|
+
height: 48,
|
|
428
|
+
template: (user) => `<div class="user">${user.name}</div>`,
|
|
429
|
+
},
|
|
430
|
+
items: users,
|
|
431
|
+
selection: { mode: 'single' },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Optional: subscribe to events with automatic cleanup
|
|
435
|
+
useVListEvent(instanceRef, 'selection:change', ({ selected }) => {
|
|
436
|
+
console.log('Selected:', selected);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div
|
|
441
|
+
ref={containerRef}
|
|
442
|
+
style={{ height: 400 }}
|
|
443
|
+
onClick={() => instanceRef.current?.scrollToIndex(0)}
|
|
444
|
+
/>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`useVList` returns:
|
|
450
|
+
- `containerRef` — attach to your container `<div>`
|
|
451
|
+
- `instanceRef` — ref to the `VList` instance (populated after mount)
|
|
452
|
+
- `getInstance()` — stable helper to access the instance
|
|
453
|
+
|
|
454
|
+
Items auto-sync when `config.items` changes by reference.
|
|
455
|
+
|
|
456
|
+
#### Vue
|
|
457
|
+
|
|
458
|
+
```vue
|
|
459
|
+
<template>
|
|
460
|
+
<div ref="containerRef" style="height: 400px" />
|
|
461
|
+
</template>
|
|
462
|
+
|
|
463
|
+
<script setup lang="ts">
|
|
464
|
+
import { useVList, useVListEvent } from 'vlist/vue';
|
|
465
|
+
import { ref } from 'vue';
|
|
466
|
+
|
|
467
|
+
const users = ref([
|
|
468
|
+
{ id: 1, name: 'Alice' },
|
|
469
|
+
{ id: 2, name: 'Bob' },
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
const { containerRef, instance } = useVList({
|
|
473
|
+
item: {
|
|
474
|
+
height: 48,
|
|
475
|
+
template: (user) => `<div class="user">${user.name}</div>`,
|
|
476
|
+
},
|
|
477
|
+
items: users.value,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Optional: subscribe to events with automatic cleanup
|
|
481
|
+
useVListEvent(instance, 'selection:change', ({ selected }) => {
|
|
482
|
+
console.log('Selected:', selected);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
function jumpToTop() {
|
|
486
|
+
instance.value?.scrollToIndex(0);
|
|
487
|
+
}
|
|
488
|
+
</script>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
`useVList` accepts a plain config or a reactive `Ref<Config>`. When using a ref, items are watched and synced automatically.
|
|
492
|
+
|
|
493
|
+
#### Svelte
|
|
494
|
+
|
|
495
|
+
```svelte
|
|
496
|
+
<script>
|
|
497
|
+
import { vlist, onVListEvent } from 'vlist/svelte';
|
|
498
|
+
|
|
499
|
+
let instance;
|
|
500
|
+
let unsubs = [];
|
|
501
|
+
|
|
502
|
+
const options = {
|
|
503
|
+
config: {
|
|
504
|
+
item: {
|
|
505
|
+
height: 48,
|
|
506
|
+
template: (user) => `<div class="user">${user.name}</div>`,
|
|
507
|
+
},
|
|
508
|
+
items: users,
|
|
509
|
+
selection: { mode: 'single' },
|
|
510
|
+
},
|
|
511
|
+
onInstance: (inst) => {
|
|
512
|
+
instance = inst;
|
|
513
|
+
unsubs.push(
|
|
514
|
+
onVListEvent(inst, 'selection:change', ({ selected }) => {
|
|
515
|
+
console.log('Selected:', selected);
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
import { onDestroy } from 'svelte';
|
|
522
|
+
onDestroy(() => unsubs.forEach(fn => fn()));
|
|
523
|
+
</script>
|
|
524
|
+
|
|
525
|
+
<div use:vlist={options} style="height: 400px" />
|
|
526
|
+
<button on:click={() => instance?.scrollToIndex(0)}>Jump to top</button>
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The `vlist` action follows the standard Svelte `use:` directive contract. It works with both Svelte 4 and 5 with zero Svelte imports. Pass reactive options via `$:` to trigger updates automatically.
|
|
530
|
+
|
|
531
|
+
### With Custom Template
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
const list = createVList({
|
|
535
|
+
container: '#my-list',
|
|
536
|
+
item: {
|
|
537
|
+
height: 72,
|
|
538
|
+
template: (item, index, { selected, focused }) => {
|
|
539
|
+
// Return an HTMLElement for more control
|
|
540
|
+
const el = document.createElement('div');
|
|
541
|
+
el.className = 'item-content';
|
|
542
|
+
el.innerHTML = `
|
|
543
|
+
<img src="${item.avatar}" class="avatar avatar--large" />
|
|
544
|
+
<div class="item-details">
|
|
545
|
+
<div class="item-name">${item.name}</div>
|
|
546
|
+
<div class="item-email">${item.email}</div>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="item-role">${item.role}</div>
|
|
549
|
+
`;
|
|
550
|
+
return el;
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
items: users,
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## API Reference
|
|
558
|
+
|
|
559
|
+
### Methods
|
|
560
|
+
|
|
561
|
+
#### Data Management
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
list.setItems(items: T[]) // Replace all items
|
|
565
|
+
list.appendItems(items: T[]) // Add items to end
|
|
566
|
+
list.prependItems(items: T[]) // Add items to start
|
|
567
|
+
list.updateItem(id, updates) // Update item by ID
|
|
568
|
+
list.removeItem(id) // Remove item by ID
|
|
569
|
+
list.reload() // Reload from adapter
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### Scrolling
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
list.scrollToIndex(index, align?) // Scroll to index ('start' | 'center' | 'end')
|
|
576
|
+
list.scrollToIndex(index, options?) // Scroll with options (smooth scrolling)
|
|
577
|
+
list.scrollToItem(id, align?) // Scroll to item by ID
|
|
578
|
+
list.scrollToItem(id, options?) // Scroll to item with options
|
|
579
|
+
list.cancelScroll() // Cancel in-progress smooth scroll
|
|
580
|
+
list.getScrollPosition() // Get current scroll position
|
|
581
|
+
list.getScrollSnapshot() // Get snapshot for save/restore
|
|
582
|
+
list.restoreScroll(snapshot) // Restore position (and selection) from snapshot
|
|
583
|
+
|
|
584
|
+
// ScrollToOptions: { align?, behavior?: 'auto' | 'smooth', duration? }
|
|
585
|
+
// Example: list.scrollToIndex(500, { align: 'center', behavior: 'smooth' })
|
|
586
|
+
// ScrollSnapshot: { index, offsetInItem, selectedIds? } — JSON-serializable
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
#### Selection
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
list.select(...ids) // Select items
|
|
593
|
+
list.deselect(...ids) // Deselect items
|
|
594
|
+
list.toggleSelect(id) // Toggle selection
|
|
595
|
+
list.selectAll() // Select all
|
|
596
|
+
list.clearSelection() // Clear selection
|
|
597
|
+
list.getSelected() // Get selected IDs
|
|
598
|
+
list.getSelectedItems() // Get selected items
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### Events
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
list.on(event, handler) // Subscribe to event
|
|
605
|
+
list.off(event, handler) // Unsubscribe from event
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
#### Lifecycle
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
list.destroy() // Cleanup and remove
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Properties
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
list.element // Root DOM element
|
|
618
|
+
list.items // Current items (readonly)
|
|
619
|
+
list.total // Total item count
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Events
|
|
623
|
+
|
|
624
|
+
| Event | Payload | Description |
|
|
625
|
+
|-------|---------|-------------|
|
|
626
|
+
| `item:click` | `{ item, index, event }` | Item was clicked |
|
|
627
|
+
| `selection:change` | `{ selected, items }` | Selection changed |
|
|
628
|
+
| `scroll` | `{ scrollTop, direction }` | Scroll position changed |
|
|
629
|
+
| `range:change` | `{ range }` | Visible range changed |
|
|
630
|
+
| `resize` | `{ height, width }` | Container was resized |
|
|
631
|
+
| `load:start` | `{ offset, limit }` | Data loading started |
|
|
632
|
+
| `load:end` | `{ items, total }` | Data loading completed |
|
|
633
|
+
| `error` | `{ error, context }` | Error occurred |
|
|
634
|
+
|
|
635
|
+
## Keyboard Navigation & Accessibility
|
|
636
|
+
|
|
637
|
+
vlist implements the [WAI-ARIA Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) for full screen reader and keyboard support.
|
|
638
|
+
|
|
639
|
+
### Keyboard Shortcuts
|
|
640
|
+
|
|
641
|
+
When selection is enabled, the list supports full keyboard navigation:
|
|
642
|
+
|
|
643
|
+
| Key | Action |
|
|
644
|
+
|-----|--------|
|
|
645
|
+
| `↑` / `↓` | Move focus up/down |
|
|
646
|
+
| `Home` | Move focus to first item |
|
|
647
|
+
| `End` | Move focus to last item |
|
|
648
|
+
| `Space` / `Enter` | Toggle selection on focused item |
|
|
649
|
+
| `Tab` | Move focus into / out of the list |
|
|
650
|
+
|
|
651
|
+
### ARIA Attributes
|
|
652
|
+
|
|
653
|
+
| Attribute | Element | Purpose |
|
|
654
|
+
|-----------|---------|---------|
|
|
655
|
+
| `role="listbox"` | Root | Identifies the widget as a list of selectable items |
|
|
656
|
+
| `role="option"` | Each item | Identifies each item as a selectable option |
|
|
657
|
+
| `aria-setsize` | Each item | Total item count — screen readers announce "item 5 of 10,000" |
|
|
658
|
+
| `aria-posinset` | Each item | 1-based position within the list |
|
|
659
|
+
| `aria-activedescendant` | Root | Points to the focused item's ID for screen reader tracking |
|
|
660
|
+
| `aria-selected` | Each item | Reflects selection state |
|
|
661
|
+
| `aria-busy` | Root | Present during async data loading |
|
|
662
|
+
| `aria-label` | Root | Set via `ariaLabel` config option |
|
|
663
|
+
|
|
664
|
+
A visually-hidden **live region** (`aria-live="polite"`) announces selection changes (e.g., "3 items selected").
|
|
665
|
+
|
|
666
|
+
Each item receives a unique `id` (`vlist-{instance}-item-{index}`) safe for multiple lists per page.
|
|
667
|
+
|
|
668
|
+
> 📖 Full documentation: [docs/accessibility.md](docs/accessibility.md)
|
|
669
|
+
|
|
670
|
+
## Styling
|
|
671
|
+
|
|
672
|
+
### Default Styles
|
|
673
|
+
|
|
674
|
+
Import the default styles:
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
import 'vlist/styles';
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Optional extras (variants, loading states, animations):
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
import 'vlist/styles/extras';
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### CSS Classes
|
|
687
|
+
|
|
688
|
+
The component uses these CSS class names:
|
|
689
|
+
|
|
690
|
+
- `.vlist` - Root container
|
|
691
|
+
- `.vlist-viewport` - Scrollable viewport
|
|
692
|
+
- `.vlist-content` - Content container (sets total height)
|
|
693
|
+
- `.vlist-items` - Items container
|
|
694
|
+
- `.vlist-item` - Individual item
|
|
695
|
+
- `.vlist-item--selected` - Selected item
|
|
696
|
+
- `.vlist-item--focused` - Focused item (keyboard nav)
|
|
697
|
+
- `.vlist--grid` - Grid layout modifier
|
|
698
|
+
- `.vlist-grid-item` - Grid item (positioned with `translate(x, y)`)
|
|
699
|
+
- `.vlist--grouped` - Grouped list modifier
|
|
700
|
+
- `.vlist-sticky-header` - Sticky header overlay
|
|
701
|
+
- `.vlist-live-region` - Visually-hidden live region for screen reader announcements
|
|
702
|
+
- `.vlist--scrolling` - Applied during active scroll (disables transitions)
|
|
703
|
+
|
|
704
|
+
### Variants
|
|
705
|
+
|
|
706
|
+
Import `vlist/styles/extras` for these variant classes:
|
|
707
|
+
|
|
708
|
+
```html
|
|
709
|
+
<!-- Compact spacing -->
|
|
710
|
+
<div class="vlist vlist--compact">...</div>
|
|
711
|
+
|
|
712
|
+
<!-- Comfortable spacing -->
|
|
713
|
+
<div class="vlist vlist--comfortable">...</div>
|
|
714
|
+
|
|
715
|
+
<!-- No borders -->
|
|
716
|
+
<div class="vlist vlist--borderless">...</div>
|
|
717
|
+
|
|
718
|
+
<!-- Striped rows -->
|
|
719
|
+
<div class="vlist vlist--striped">...</div>
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### CSS Custom Properties
|
|
723
|
+
|
|
724
|
+
All visual aspects can be customized via CSS custom properties:
|
|
725
|
+
|
|
726
|
+
```css
|
|
727
|
+
:root {
|
|
728
|
+
--vlist-bg: #ffffff;
|
|
729
|
+
--vlist-bg-hover: #f9fafb;
|
|
730
|
+
--vlist-bg-selected: #eff6ff;
|
|
731
|
+
--vlist-border: #e5e7eb;
|
|
732
|
+
--vlist-text: #111827;
|
|
733
|
+
--vlist-focus-ring: #3b82f6;
|
|
734
|
+
--vlist-item-padding-x: 1rem;
|
|
735
|
+
--vlist-item-padding-y: 0.75rem;
|
|
736
|
+
--vlist-border-radius: 0.5rem;
|
|
737
|
+
--vlist-transition-duration: 150ms;
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Dark mode is supported automatically via `prefers-color-scheme: dark` or the `.dark` class.
|
|
742
|
+
|
|
743
|
+
## TypeScript
|
|
744
|
+
|
|
745
|
+
Full TypeScript support with generics:
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
interface User {
|
|
749
|
+
id: number;
|
|
750
|
+
name: string;
|
|
751
|
+
email: string;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const list = createVList<User>({
|
|
755
|
+
container: '#users',
|
|
756
|
+
item: {
|
|
757
|
+
height: 48,
|
|
758
|
+
template: (user) => `<div>${user.name} - ${user.email}</div>`,
|
|
759
|
+
},
|
|
760
|
+
items: users,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Fully typed
|
|
764
|
+
list.on('item:click', ({ item }) => {
|
|
765
|
+
console.log(item.email); // TypeScript knows this is a User
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
## Performance
|
|
770
|
+
|
|
771
|
+
vlist is designed for maximum performance with extensive built-in optimizations:
|
|
772
|
+
|
|
773
|
+
- **Virtual rendering** - Only visible items + overscan buffer are in the DOM
|
|
774
|
+
- **Element pooling** - DOM elements are recycled via `createElementPool()`, reducing GC pressure
|
|
775
|
+
- **Zero-allocation scroll hot path** - No object allocations per scroll frame; in-place range mutation
|
|
776
|
+
- **RAF-throttled native scroll** - At most one scroll processing per animation frame
|
|
777
|
+
- **CSS containment** - `contain: layout style` on items container, `contain: content` + `will-change: transform` on items
|
|
778
|
+
- **Scroll transition suppression** - `.vlist--scrolling` class disables CSS transitions during active scroll
|
|
779
|
+
- **Circular buffer velocity tracker** - Pre-allocated buffer, zero allocations during scroll
|
|
780
|
+
- **Targeted keyboard focus render** - Arrow keys update only 2 affected items instead of all visible items
|
|
781
|
+
- **Batched LRU timestamps** - Single `Date.now()` per render cycle instead of per-item
|
|
782
|
+
- **DocumentFragment batching** - New elements appended in a single DOM operation
|
|
783
|
+
- **Split CSS** - Core styles (5.6 KB) separated from optional extras (1.8 KB)
|
|
784
|
+
- **Configurable velocity-based loading** - Skip, preload, or defer loading based on scroll speed
|
|
785
|
+
- **Compression for 1M+ items** - Automatic scroll space compression when content exceeds browser height limits
|
|
786
|
+
|
|
787
|
+
### Benchmark Results
|
|
788
|
+
|
|
789
|
+
Measured in Chrome (10-core Mac, 60Hz display) via the [live benchmark page](https://vlist.dev/benchmarks/):
|
|
790
|
+
|
|
791
|
+
| Metric | 10K items | 1M items |
|
|
792
|
+
|--------|-----------|----------|
|
|
793
|
+
| Initial render | ~32ms | ~135ms |
|
|
794
|
+
| Scroll FPS | 60fps | 61fps |
|
|
795
|
+
| Frame budget (avg) | 2.1ms | 1.9ms |
|
|
796
|
+
| Frame budget (p95) | 4.2ms | 8.7ms |
|
|
797
|
+
| Dropped frames | 0% | 0% |
|
|
798
|
+
| scrollToIndex | ~166ms | ~82ms |
|
|
799
|
+
| Memory (scroll delta) | 0 MB | 0 MB |
|
|
800
|
+
|
|
801
|
+
Zero dropped frames and zero memory growth during sustained scrolling — even at 1M items.
|
|
802
|
+
|
|
803
|
+
For the full optimization guide, see [docs/optimization.md](docs/optimization.md).
|
|
804
|
+
|
|
805
|
+
## Browser Support
|
|
806
|
+
|
|
807
|
+
- Chrome 60+
|
|
808
|
+
- Firefox 55+
|
|
809
|
+
- Safari 12+
|
|
810
|
+
- Edge 79+
|
|
811
|
+
|
|
812
|
+
## Contributing
|
|
813
|
+
|
|
814
|
+
Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
# Install dependencies
|
|
818
|
+
bun install
|
|
819
|
+
|
|
820
|
+
# Run development build
|
|
821
|
+
bun run dev
|
|
822
|
+
|
|
823
|
+
# Run tests
|
|
824
|
+
bun test
|
|
825
|
+
|
|
826
|
+
# Type check
|
|
827
|
+
bun run typecheck
|
|
828
|
+
|
|
829
|
+
# Build for production
|
|
830
|
+
bun run build
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
## License
|
|
834
|
+
|
|
835
|
+
MIT © [Floor](https://github.com/floor)
|
|
836
|
+
|
|
837
|
+
## Credits
|
|
838
|
+
|
|
839
|
+
Inspired by the [mtrl-addons](https://github.com/floor/mtrl-addons) vlist component.
|