@floor/vlist 0.7.5 â 0.7.7
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.md +148 -846
- package/dist/features/scale/plugin.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +5 -16
- package/dist/async/index.js +0 -1
- package/dist/builder/index.js +0 -1
- package/dist/core/full.d.ts +0 -24
- package/dist/grid/index.js +0 -1
- package/dist/page/index.js +0 -1
- package/dist/scale/index.js +0 -1
- package/dist/scrollbar/index.js +0 -1
- package/dist/sections/index.js +0 -1
- package/dist/selection/index.js +0 -1
- package/dist/snapshots/index.js +0 -1
package/README.md
CHANGED
|
@@ -7,32 +7,15 @@ Lightweight, high-performance virtual list with zero dependencies and optimal tr
|
|
|
7
7
|
[](https://github.com/floor/vlist)
|
|
8
8
|
[](https://github.com/floor/vlist/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- ð **Async loading** - Built-in lazy loading with adapters
|
|
20
|
-
- â
**Selection** - Single and multiple selection modes
|
|
21
|
-
- ð **Sticky headers** - Grouped lists with sticky section headers
|
|
22
|
-
- ðŠ **Page scrolling** - Document-level scrolling mode
|
|
23
|
-
- ð **Wrap navigation** - Circular scrolling for wizards
|
|
24
|
-
- ðŽ **Reverse mode** - Chat UI with auto-scroll and history loading
|
|
25
|
-
- âïļ **Scale to millions** - Handle 1M+ items with automatic compression
|
|
26
|
-
- ðĻ **Customizable** - Beautiful, customizable styles
|
|
27
|
-
- âŋ **Accessible** - Full WAI-ARIA support and keyboard navigation
|
|
28
|
-
- ð **Framework adapters** - React, Vue, and Svelte support
|
|
29
|
-
- ðą **Mobile optimized** - Touch-friendly with momentum scrolling
|
|
30
|
-
|
|
31
|
-
## Sandbox & Documentation
|
|
32
|
-
|
|
33
|
-
Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
|
|
34
|
-
|
|
35
|
-
**30+ examples** with multi-framework implementations (JavaScript, React, Vue, Svelte)
|
|
10
|
+
- **Zero dependencies** â no external libraries
|
|
11
|
+
- **8â12 KB gzipped** â pay only for features you use (vs 20 KB+ monolithic alternatives)
|
|
12
|
+
- **Builder API** â composable plugins with perfect tree-shaking
|
|
13
|
+
- **Grid, sections, async, selection, scale** â all opt-in
|
|
14
|
+
- **Horizontal, reverse, page-scroll, wrap** â every layout mode
|
|
15
|
+
- **Accessible** â WAI-ARIA, keyboard navigation, screen-reader friendly
|
|
16
|
+
- **React, Vue, Svelte** â framework adapters available
|
|
17
|
+
|
|
18
|
+
**30+ interactive examples â [vlist.dev](https://vlist.dev)**
|
|
36
19
|
|
|
37
20
|
## Installation
|
|
38
21
|
|
|
@@ -40,13 +23,11 @@ Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
|
|
|
40
23
|
npm install @floor/vlist
|
|
41
24
|
```
|
|
42
25
|
|
|
43
|
-
> **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
|
|
44
|
-
|
|
45
26
|
## Quick Start
|
|
46
27
|
|
|
47
28
|
```typescript
|
|
48
|
-
import { vlist } from 'vlist'
|
|
49
|
-
import 'vlist/styles'
|
|
29
|
+
import { vlist } from '@floor/vlist'
|
|
30
|
+
import '@floor/vlist/styles'
|
|
50
31
|
|
|
51
32
|
const list = vlist({
|
|
52
33
|
container: '#my-list',
|
|
@@ -59,87 +40,57 @@ const list = vlist({
|
|
|
59
40
|
height: 48,
|
|
60
41
|
template: (item) => `<div>${item.name}</div>`,
|
|
61
42
|
},
|
|
62
|
-
}).build()
|
|
43
|
+
}).build()
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
list.
|
|
66
|
-
list.
|
|
67
|
-
list.on('item:click', ({ item }) => console.log(item));
|
|
45
|
+
list.scrollToIndex(10)
|
|
46
|
+
list.setItems(newItems)
|
|
47
|
+
list.on('item:click', ({ item }) => console.log(item))
|
|
68
48
|
```
|
|
69
49
|
|
|
70
|
-
**Bundle:** ~8 KB gzipped
|
|
71
|
-
|
|
72
50
|
## Builder Pattern
|
|
73
51
|
|
|
74
|
-
|
|
52
|
+
Start with the base, add only what you need:
|
|
75
53
|
|
|
76
54
|
```typescript
|
|
77
|
-
import { vlist, withGrid, withSections, withSelection } from 'vlist'
|
|
55
|
+
import { vlist, withGrid, withSections, withSelection } from '@floor/vlist'
|
|
78
56
|
|
|
79
57
|
const list = vlist({
|
|
80
58
|
container: '#app',
|
|
81
59
|
items: photos,
|
|
82
|
-
item: {
|
|
83
|
-
height: 200,
|
|
84
|
-
template: renderPhoto,
|
|
85
|
-
},
|
|
60
|
+
item: { height: 200, template: renderPhoto },
|
|
86
61
|
})
|
|
87
62
|
.use(withGrid({ columns: 4, gap: 16 }))
|
|
88
|
-
.use(withSections({
|
|
63
|
+
.use(withSections({
|
|
89
64
|
getGroupForIndex: (i) => photos[i].category,
|
|
90
65
|
headerHeight: 40,
|
|
91
66
|
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
92
67
|
}))
|
|
93
68
|
.use(withSelection({ mode: 'multiple' }))
|
|
94
|
-
.build()
|
|
69
|
+
.build()
|
|
95
70
|
```
|
|
96
71
|
|
|
97
|
-
|
|
72
|
+
### Plugins
|
|
98
73
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Plugin | Cost | Description |
|
|
74
|
+
| Plugin | Size | Description |
|
|
102
75
|
|--------|------|-------------|
|
|
103
|
-
| **Base** | 7.7 KB
|
|
76
|
+
| **Base** | 7.7 KB | Core virtualization |
|
|
104
77
|
| `withGrid()` | +4.0 KB | 2D grid layout |
|
|
105
78
|
| `withSections()` | +4.6 KB | Grouped lists with sticky/inline headers |
|
|
106
|
-
| `withAsync()` | +5.3 KB |
|
|
107
|
-
| `withSelection()` | +2.3 KB | Single/multiple
|
|
108
|
-
| `withScale()` | +2.2 KB |
|
|
79
|
+
| `withAsync()` | +5.3 KB | Lazy loading with adapters |
|
|
80
|
+
| `withSelection()` | +2.3 KB | Single/multiple selection + keyboard nav |
|
|
81
|
+
| `withScale()` | +2.2 KB | 1M+ items via scroll compression |
|
|
109
82
|
| `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
|
|
110
83
|
| `withPage()` | +0.9 KB | Document-level scrolling |
|
|
111
|
-
| `withSnapshots()` |
|
|
112
|
-
|
|
113
|
-
**Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
|
|
84
|
+
| `withSnapshots()` | included | Scroll save/restore |
|
|
114
85
|
|
|
115
86
|
## Examples
|
|
116
87
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
import { vlist } from 'vlist';
|
|
121
|
-
|
|
122
|
-
const list = vlist({
|
|
123
|
-
container: '#list',
|
|
124
|
-
items: users,
|
|
125
|
-
item: {
|
|
126
|
-
height: 64,
|
|
127
|
-
template: (user) => `
|
|
128
|
-
<div class="user">
|
|
129
|
-
<img src="${user.avatar}" />
|
|
130
|
-
<span>${user.name}</span>
|
|
131
|
-
</div>
|
|
132
|
-
`,
|
|
133
|
-
},
|
|
134
|
-
}).build();
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**Bundle:** 8.2 KB gzipped
|
|
88
|
+
More examples at **[vlist.dev](https://vlist.dev)**.
|
|
138
89
|
|
|
139
90
|
### Grid Layout
|
|
140
91
|
|
|
141
92
|
```typescript
|
|
142
|
-
import { vlist, withGrid, withScrollbar } from 'vlist'
|
|
93
|
+
import { vlist, withGrid, withScrollbar } from '@floor/vlist'
|
|
143
94
|
|
|
144
95
|
const gallery = vlist({
|
|
145
96
|
container: '#gallery',
|
|
@@ -156,887 +107,238 @@ const gallery = vlist({
|
|
|
156
107
|
})
|
|
157
108
|
.use(withGrid({ columns: 4, gap: 16 }))
|
|
158
109
|
.use(withScrollbar({ autoHide: true }))
|
|
159
|
-
.build()
|
|
110
|
+
.build()
|
|
160
111
|
```
|
|
161
112
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
Grid mode virtualizes by **rows** - only visible rows are in the DOM. Each item is positioned with `translate(x, y)` for GPU-accelerated rendering.
|
|
165
|
-
|
|
166
|
-
### Sticky Headers (Contact List)
|
|
113
|
+
### Sticky Headers
|
|
167
114
|
|
|
168
115
|
```typescript
|
|
169
|
-
import { vlist, withSections } from 'vlist'
|
|
116
|
+
import { vlist, withSections } from '@floor/vlist'
|
|
170
117
|
|
|
171
118
|
const contacts = vlist({
|
|
172
119
|
container: '#contacts',
|
|
173
|
-
items: sortedContacts,
|
|
120
|
+
items: sortedContacts,
|
|
174
121
|
item: {
|
|
175
122
|
height: 56,
|
|
176
123
|
template: (contact) => `<div>${contact.name}</div>`,
|
|
177
124
|
},
|
|
178
125
|
})
|
|
179
126
|
.use(withSections({
|
|
180
|
-
getGroupForIndex: (i) =>
|
|
127
|
+
getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
|
|
181
128
|
headerHeight: 36,
|
|
182
129
|
headerTemplate: (letter) => `<div class="header">${letter}</div>`,
|
|
183
|
-
sticky: true,
|
|
130
|
+
sticky: true,
|
|
184
131
|
}))
|
|
185
|
-
.build()
|
|
132
|
+
.build()
|
|
186
133
|
```
|
|
187
134
|
|
|
188
|
-
**Bundle:** 12.3 KB gzipped
|
|
189
|
-
|
|
190
135
|
Set `sticky: false` for inline headers (iMessage/WhatsApp style).
|
|
191
136
|
|
|
192
|
-
###
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import { vlist, withSections } from 'vlist';
|
|
196
|
-
|
|
197
|
-
const chat = vlist({
|
|
198
|
-
container: '#messages',
|
|
199
|
-
reverse: true, // Start at bottom, newest messages visible
|
|
200
|
-
items: messages, // Chronological order (oldest first)
|
|
201
|
-
item: {
|
|
202
|
-
height: (i) => messages[i].height || 60,
|
|
203
|
-
template: (msg) => `<div class="message">${msg.text}</div>`,
|
|
204
|
-
},
|
|
205
|
-
})
|
|
206
|
-
.use(withSections({
|
|
207
|
-
getGroupForIndex: (i) => {
|
|
208
|
-
const date = new Date(messages[i].timestamp);
|
|
209
|
-
return date.toLocaleDateString(); // "Jan 15", "Jan 16", etc.
|
|
210
|
-
},
|
|
211
|
-
headerHeight: 32,
|
|
212
|
-
headerTemplate: (date) => `<div class="date-header">${date}</div>`,
|
|
213
|
-
sticky: false, // Inline date headers (iMessage style)
|
|
214
|
-
}))
|
|
215
|
-
.build();
|
|
216
|
-
|
|
217
|
-
// New messages - auto-scrolls to bottom
|
|
218
|
-
chat.appendItems([newMessage]);
|
|
219
|
-
|
|
220
|
-
// Load history - preserves scroll position
|
|
221
|
-
chat.prependItems(olderMessages);
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Bundle:** 11.9 KB gzipped
|
|
225
|
-
|
|
226
|
-
Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
|
|
227
|
-
|
|
228
|
-
### Large Datasets (1M+ Items)
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
import { vlist, withScale, withScrollbar } from 'vlist';
|
|
232
|
-
|
|
233
|
-
const bigList = vlist({
|
|
234
|
-
container: '#big-list',
|
|
235
|
-
items: generateItems(5_000_000),
|
|
236
|
-
item: {
|
|
237
|
-
height: 48,
|
|
238
|
-
template: (item) => `<div>#${item.id}: ${item.name}</div>`,
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
.use(withScale()) // Auto-activates when height > 16.7M pixels
|
|
242
|
-
.use(withScrollbar({ autoHide: true }))
|
|
243
|
-
.build();
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Bundle:** 9.9 KB gzipped
|
|
247
|
-
|
|
248
|
-
The scale plugin automatically compresses scroll space when total height exceeds browser limits (~16.7M pixels), enabling smooth scrolling through millions of items.
|
|
249
|
-
|
|
250
|
-
### Async Loading with Pagination
|
|
137
|
+
### Async Loading
|
|
251
138
|
|
|
252
139
|
```typescript
|
|
253
|
-
import { vlist, withAsync } from 'vlist'
|
|
140
|
+
import { vlist, withAsync } from '@floor/vlist'
|
|
254
141
|
|
|
255
142
|
const list = vlist({
|
|
256
143
|
container: '#list',
|
|
257
144
|
item: {
|
|
258
145
|
height: 64,
|
|
259
|
-
template: (item) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
},
|
|
146
|
+
template: (item) => item
|
|
147
|
+
? `<div>${item.name}</div>`
|
|
148
|
+
: `<div class="placeholder">LoadingâĶ</div>`,
|
|
263
149
|
},
|
|
264
150
|
})
|
|
265
151
|
.use(withAsync({
|
|
266
152
|
adapter: {
|
|
267
153
|
read: async ({ offset, limit }) => {
|
|
268
|
-
const
|
|
269
|
-
const data = await
|
|
270
|
-
return {
|
|
271
|
-
items: data.items,
|
|
272
|
-
total: data.total,
|
|
273
|
-
hasMore: data.hasMore,
|
|
274
|
-
};
|
|
154
|
+
const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
|
|
155
|
+
const data = await res.json()
|
|
156
|
+
return { items: data.items, total: data.total, hasMore: data.hasMore }
|
|
275
157
|
},
|
|
276
158
|
},
|
|
277
|
-
loading: {
|
|
278
|
-
cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
|
|
279
|
-
},
|
|
280
159
|
}))
|
|
281
|
-
.build()
|
|
160
|
+
.build()
|
|
282
161
|
```
|
|
283
162
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
The async plugin shows placeholders for unloaded items and fetches data as you scroll. Velocity-aware loading cancels requests when scrolling fast.
|
|
163
|
+
### More Patterns
|
|
287
164
|
|
|
288
|
-
|
|
165
|
+
| Pattern | Key options |
|
|
166
|
+
|---------|------------|
|
|
167
|
+
| **Chat UI** | `reverse: true` + `withSections({ sticky: false })` |
|
|
168
|
+
| **Horizontal carousel** | `direction: 'horizontal'`, `item.width` |
|
|
169
|
+
| **Page-level scroll** | `withPage()` |
|
|
170
|
+
| **1M+ items** | `withScale()` â auto-compresses scroll space |
|
|
171
|
+
| **Wrap navigation** | `scroll: { wrap: true }` |
|
|
172
|
+
| **Variable heights** | `item: { height: (index) => heights[index] }` |
|
|
289
173
|
|
|
290
|
-
|
|
291
|
-
import { vlist, withPage } from 'vlist';
|
|
292
|
-
|
|
293
|
-
const list = vlist({
|
|
294
|
-
container: '#list',
|
|
295
|
-
items: articles,
|
|
296
|
-
item: {
|
|
297
|
-
height: 200,
|
|
298
|
-
template: (article) => `<article>...</article>`,
|
|
299
|
-
},
|
|
300
|
-
})
|
|
301
|
-
.use(withPage()) // Uses document scroll instead of container
|
|
302
|
-
.build();
|
|
303
|
-
```
|
|
174
|
+
See **[vlist.dev](https://vlist.dev)** for live demos of each.
|
|
304
175
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
308
|
-
|
|
309
|
-
### Horizontal Carousel
|
|
176
|
+
## API
|
|
310
177
|
|
|
311
178
|
```typescript
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const carousel = vlist({
|
|
315
|
-
container: '#carousel',
|
|
316
|
-
direction: 'horizontal',
|
|
317
|
-
items: cards,
|
|
318
|
-
item: {
|
|
319
|
-
width: 300, // Required for horizontal
|
|
320
|
-
height: 400, // Optional (can use CSS)
|
|
321
|
-
template: (card) => `<div class="card">...</div>`,
|
|
322
|
-
},
|
|
323
|
-
scroll: {
|
|
324
|
-
wrap: true, // Circular scrolling
|
|
325
|
-
},
|
|
326
|
-
}).build();
|
|
179
|
+
const list = vlist(config).use(...plugins).build()
|
|
327
180
|
```
|
|
328
181
|
|
|
329
|
-
|
|
182
|
+
### Data
|
|
330
183
|
|
|
331
|
-
|
|
184
|
+
| Method | Description |
|
|
185
|
+
|--------|-------------|
|
|
186
|
+
| `list.setItems(items)` | Replace all items |
|
|
187
|
+
| `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
|
|
188
|
+
| `list.prependItems(items)` | Add to start (preserves scroll position) |
|
|
189
|
+
| `list.updateItem(id, partial)` | Update a single item |
|
|
190
|
+
| `list.removeItem(id)` | Remove by ID |
|
|
191
|
+
| `list.reload()` | Re-fetch from adapter (async) |
|
|
332
192
|
|
|
333
|
-
|
|
334
|
-
import { vlist, withSelection } from 'vlist';
|
|
193
|
+
### Navigation
|
|
335
194
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
})
|
|
347
|
-
.use(withSelection({
|
|
348
|
-
mode: 'multiple',
|
|
349
|
-
initial: [1, 5, 10], // Pre-select items
|
|
350
|
-
}))
|
|
351
|
-
.build();
|
|
352
|
-
|
|
353
|
-
// Selection API
|
|
354
|
-
list.selectItem(5);
|
|
355
|
-
list.deselectItem(5);
|
|
356
|
-
list.toggleSelection(5);
|
|
357
|
-
list.getSelectedIds(); // [1, 5, 10]
|
|
358
|
-
list.clearSelection();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Bundle:** 10.0 KB gzipped
|
|
362
|
-
|
|
363
|
-
Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
|
|
364
|
-
|
|
365
|
-
### Variable Heights (Chat Messages)
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
import { vlist } from 'vlist';
|
|
369
|
-
|
|
370
|
-
const list = vlist({
|
|
371
|
-
container: '#messages',
|
|
372
|
-
items: messages,
|
|
373
|
-
item: {
|
|
374
|
-
height: (index) => {
|
|
375
|
-
// Heights computed from actual DOM measurements
|
|
376
|
-
return messages[index].measuredHeight || 60;
|
|
377
|
-
},
|
|
378
|
-
template: (msg) => `
|
|
379
|
-
<div class="message">
|
|
380
|
-
<div class="author">${msg.user}</div>
|
|
381
|
-
<div class="text">${msg.text}</div>
|
|
382
|
-
</div>
|
|
383
|
-
`,
|
|
384
|
-
},
|
|
385
|
-
}).build();
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
**Bundle:** 10.9 KB gzipped
|
|
389
|
-
|
|
390
|
-
Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
|
|
391
|
-
|
|
392
|
-
## API Reference
|
|
393
|
-
|
|
394
|
-
### Core Methods
|
|
395
|
-
|
|
396
|
-
```typescript
|
|
397
|
-
const list = vlist(config).use(...plugins).build();
|
|
398
|
-
|
|
399
|
-
// Data manipulation
|
|
400
|
-
list.setItems(items: T[]): void
|
|
401
|
-
list.appendItems(items: T[]): void
|
|
402
|
-
list.prependItems(items: T[]): void
|
|
403
|
-
list.updateItem(id: string | number, item: Partial<T>): boolean
|
|
404
|
-
list.removeItem(id: string | number): boolean
|
|
405
|
-
|
|
406
|
-
// Navigation
|
|
407
|
-
list.scrollToIndex(index: number, align?: 'start' | 'center' | 'end'): void
|
|
408
|
-
list.scrollToIndex(index: number, options?: {
|
|
409
|
-
align?: 'start' | 'center' | 'end',
|
|
410
|
-
behavior?: 'auto' | 'smooth',
|
|
411
|
-
duration?: number
|
|
412
|
-
}): void
|
|
413
|
-
list.scrollToItem(id: string | number, align?: string): void
|
|
414
|
-
|
|
415
|
-
// State
|
|
416
|
-
list.getScrollPosition(): number
|
|
417
|
-
list.getVisibleRange(): { start: number, end: number }
|
|
418
|
-
list.getScrollSnapshot(): ScrollSnapshot
|
|
419
|
-
list.restoreScroll(snapshot: ScrollSnapshot): void
|
|
420
|
-
|
|
421
|
-
// Events
|
|
422
|
-
list.on(event: string, handler: Function): Unsubscribe
|
|
423
|
-
list.off(event: string, handler: Function): void
|
|
424
|
-
|
|
425
|
-
// Lifecycle
|
|
426
|
-
list.destroy(): void
|
|
427
|
-
|
|
428
|
-
// Properties
|
|
429
|
-
list.element: HTMLElement
|
|
430
|
-
list.items: readonly T[]
|
|
431
|
-
list.total: number
|
|
432
|
-
```
|
|
195
|
+
| Method | Description |
|
|
196
|
+
|--------|-------------|
|
|
197
|
+
| `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
|
|
198
|
+
| `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
|
|
199
|
+
| `list.scrollToItem(id, align?)` | Scroll to item by ID |
|
|
200
|
+
| `list.cancelScroll()` | Cancel smooth scroll animation |
|
|
201
|
+
| `list.getScrollPosition()` | Current scroll offset |
|
|
202
|
+
| `list.getVisibleRange()` | `{ start, end }` of visible indices |
|
|
203
|
+
| `list.getScrollSnapshot()` | Save scroll state (for SPA navigation) |
|
|
204
|
+
| `list.restoreScroll(snapshot)` | Restore saved scroll state |
|
|
433
205
|
|
|
434
|
-
### Selection
|
|
206
|
+
### Selection (with `withSelection()`)
|
|
435
207
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
list.
|
|
439
|
-
list.
|
|
440
|
-
list.
|
|
441
|
-
list.clearSelection()
|
|
442
|
-
list.getSelectedIds()
|
|
443
|
-
list.getSelectedItems()
|
|
444
|
-
list.setSelectionMode(mode: 'none' | 'single' | 'multiple'): void
|
|
445
|
-
```
|
|
208
|
+
| Method | Description |
|
|
209
|
+
|--------|-------------|
|
|
210
|
+
| `list.selectItem(id)` | Select item |
|
|
211
|
+
| `list.deselectItem(id)` | Deselect item |
|
|
212
|
+
| `list.toggleSelection(id)` | Toggle |
|
|
213
|
+
| `list.selectAll()` / `list.clearSelection()` | Bulk operations |
|
|
214
|
+
| `list.getSelectedIds()` | Array of selected IDs |
|
|
215
|
+
| `list.getSelectedItems()` | Array of selected items |
|
|
446
216
|
|
|
447
|
-
### Grid
|
|
217
|
+
### Grid (with `withGrid()`)
|
|
448
218
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
219
|
+
| Method | Description |
|
|
220
|
+
|--------|-------------|
|
|
221
|
+
| `list.updateGrid({ columns, gap })` | Update grid at runtime |
|
|
452
222
|
|
|
453
223
|
### Events
|
|
454
224
|
|
|
455
|
-
|
|
456
|
-
list.on('scroll', ({ scrollTop, direction }) => { })
|
|
457
|
-
list.on('range:change', ({ range }) => { })
|
|
458
|
-
list.on('item:click', ({ item, index, event }) => { })
|
|
459
|
-
list.on('item:dblclick', ({ item, index, event }) => { })
|
|
460
|
-
list.on('selection:change', ({ selectedIds, selectedItems }) => { })
|
|
461
|
-
list.on('load:start', ({ offset, limit }) => { })
|
|
462
|
-
list.on('load:end', ({ items, offset, total }) => { })
|
|
463
|
-
list.on('load:error', ({ error, offset, limit }) => { })
|
|
464
|
-
list.on('velocity:change', ({ velocity, reliable }) => { })
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
## Configuration
|
|
468
|
-
|
|
469
|
-
### Base Configuration
|
|
470
|
-
|
|
471
|
-
```typescript
|
|
472
|
-
interface VListConfig<T> {
|
|
473
|
-
// Required
|
|
474
|
-
container: HTMLElement | string;
|
|
475
|
-
item: {
|
|
476
|
-
height?: number | ((index: number) => number); // Required for vertical
|
|
477
|
-
width?: number | ((index: number) => number); // Required for horizontal
|
|
478
|
-
template: (item: T, index: number, state: ItemState) => string | HTMLElement;
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// Optional
|
|
482
|
-
items?: T[]; // Initial items
|
|
483
|
-
overscan?: number; // Extra items to render (default: 3)
|
|
484
|
-
direction?: 'vertical' | 'horizontal'; // Default: 'vertical'
|
|
485
|
-
reverse?: boolean; // Reverse mode for chat (default: false)
|
|
486
|
-
classPrefix?: string; // CSS class prefix (default: 'vlist')
|
|
487
|
-
ariaLabel?: string; // Accessible label
|
|
488
|
-
|
|
489
|
-
scroll?: {
|
|
490
|
-
wheel?: boolean; // Enable mouse wheel (default: true)
|
|
491
|
-
wrap?: boolean; // Circular scrolling (default: false)
|
|
492
|
-
scrollbar?: 'none'; // Hide scrollbar
|
|
493
|
-
idleTimeout?: number; // Scroll idle detection (default: 150ms)
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Plugin: `withGrid(config)`
|
|
499
|
-
|
|
500
|
-
2D grid layout with virtualized rows.
|
|
501
|
-
|
|
502
|
-
```typescript
|
|
503
|
-
interface GridConfig {
|
|
504
|
-
columns: number; // Number of columns (required)
|
|
505
|
-
gap?: number; // Gap between items in pixels (default: 0)
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
**Example:**
|
|
510
|
-
```typescript
|
|
511
|
-
.use(withGrid({ columns: 4, gap: 16 }))
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Plugin: `withSections(config)`
|
|
515
|
-
|
|
516
|
-
Grouped lists with sticky or inline headers.
|
|
517
|
-
|
|
518
|
-
```typescript
|
|
519
|
-
interface SectionsConfig {
|
|
520
|
-
getGroupForIndex: (index: number) => string;
|
|
521
|
-
headerHeight: number | ((group: string, groupIndex: number) => number);
|
|
522
|
-
headerTemplate: (group: string, groupIndex: number) => string | HTMLElement;
|
|
523
|
-
sticky?: boolean; // Default: true (Telegram style), false = inline (iMessage style)
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
**Example:**
|
|
528
|
-
```typescript
|
|
529
|
-
.use(withSections({
|
|
530
|
-
getGroupForIndex: (i) => items[i].category,
|
|
531
|
-
headerHeight: 40,
|
|
532
|
-
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
533
|
-
sticky: true,
|
|
534
|
-
}))
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
**Important:** Items must be pre-sorted by group!
|
|
538
|
-
|
|
539
|
-
### Plugin: `withSelection(config)`
|
|
540
|
-
|
|
541
|
-
Single or multiple item selection with keyboard navigation.
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
interface SelectionConfig {
|
|
545
|
-
mode: 'single' | 'multiple';
|
|
546
|
-
initial?: Array<string | number>; // Pre-selected item IDs
|
|
547
|
-
}
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
**Example:**
|
|
551
|
-
```typescript
|
|
552
|
-
.use(withSelection({
|
|
553
|
-
mode: 'multiple',
|
|
554
|
-
initial: [1, 5, 10],
|
|
555
|
-
}))
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### Plugin: `withAsync(config)`
|
|
559
|
-
|
|
560
|
-
Asynchronous data loading with lazy loading and placeholders.
|
|
561
|
-
|
|
562
|
-
```typescript
|
|
563
|
-
interface AsyncConfig {
|
|
564
|
-
adapter: {
|
|
565
|
-
read: (params: { offset: number, limit: number }) => Promise<{
|
|
566
|
-
items: T[];
|
|
567
|
-
total?: number;
|
|
568
|
-
hasMore?: boolean;
|
|
569
|
-
}>;
|
|
570
|
-
};
|
|
571
|
-
loading?: {
|
|
572
|
-
cancelThreshold?: number; // Cancel loads when scrolling fast (px/ms)
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
**Example:**
|
|
578
|
-
```typescript
|
|
579
|
-
.use(withAsync({
|
|
580
|
-
adapter: {
|
|
581
|
-
read: async ({ offset, limit }) => {
|
|
582
|
-
const response = await fetch(`/api?offset=${offset}&limit=${limit}`);
|
|
583
|
-
return response.json();
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
loading: {
|
|
587
|
-
cancelThreshold: 15, // Skip loads when scrolling > 15px/ms
|
|
588
|
-
},
|
|
589
|
-
}))
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### Plugin: `withScale()`
|
|
593
|
-
|
|
594
|
-
Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
.use(withScale()) // No config needed - auto-activates
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
**Example:**
|
|
601
|
-
```typescript
|
|
602
|
-
const bigList = vlist({
|
|
603
|
-
container: '#list',
|
|
604
|
-
items: generateItems(5_000_000),
|
|
605
|
-
item: { height: 48, template: renderItem },
|
|
606
|
-
})
|
|
607
|
-
.use(withScale())
|
|
608
|
-
.build();
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
### Plugin: `withScrollbar(config)`
|
|
612
|
-
|
|
613
|
-
Custom scrollbar with auto-hide and smooth dragging.
|
|
614
|
-
|
|
615
|
-
```typescript
|
|
616
|
-
interface ScrollbarConfig {
|
|
617
|
-
autoHide?: boolean; // Hide when not scrolling (default: false)
|
|
618
|
-
autoHideDelay?: number; // Hide delay in ms (default: 1000)
|
|
619
|
-
minThumbSize?: number; // Minimum thumb size in px (default: 20)
|
|
620
|
-
}
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
**Example:**
|
|
624
|
-
```typescript
|
|
625
|
-
.use(withScrollbar({
|
|
626
|
-
autoHide: true,
|
|
627
|
-
autoHideDelay: 1500,
|
|
628
|
-
}))
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
### Plugin: `withPage()`
|
|
632
|
-
|
|
633
|
-
Use document-level scrolling instead of container scrolling.
|
|
634
|
-
|
|
635
|
-
```typescript
|
|
636
|
-
.use(withPage()) // No config needed
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
**Example:**
|
|
640
|
-
```typescript
|
|
641
|
-
const list = vlist({
|
|
642
|
-
container: '#list',
|
|
643
|
-
items: articles,
|
|
644
|
-
item: { height: 300, template: renderArticle },
|
|
645
|
-
})
|
|
646
|
-
.use(withPage())
|
|
647
|
-
.build();
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
651
|
-
|
|
652
|
-
### Plugin: `withSnapshots()`
|
|
653
|
-
|
|
654
|
-
Scroll position save/restore for SPA navigation.
|
|
225
|
+
`list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.
|
|
655
226
|
|
|
656
227
|
```typescript
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
list.
|
|
228
|
+
list.on('scroll', ({ scrollTop, direction }) => {})
|
|
229
|
+
list.on('range:change', ({ range }) => {})
|
|
230
|
+
list.on('item:click', ({ item, index, event }) => {})
|
|
231
|
+
list.on('item:dblclick', ({ item, index, event }) => {})
|
|
232
|
+
list.on('selection:change', ({ selectedIds, selectedItems }) => {})
|
|
233
|
+
list.on('load:start', ({ offset, limit }) => {})
|
|
234
|
+
list.on('load:end', ({ items, offset, total }) => {})
|
|
235
|
+
list.on('load:error', ({ error, offset, limit }) => {})
|
|
236
|
+
list.on('velocity:change', ({ velocity, reliable }) => {})
|
|
660
237
|
```
|
|
661
238
|
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
// Save on navigation away
|
|
665
|
-
const snapshot = list.getScrollSnapshot();
|
|
666
|
-
sessionStorage.setItem('scroll', JSON.stringify(snapshot));
|
|
667
|
-
|
|
668
|
-
// Restore on return
|
|
669
|
-
const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
|
|
670
|
-
list.restoreScroll(snapshot);
|
|
671
|
-
```
|
|
239
|
+
### Properties
|
|
672
240
|
|
|
673
|
-
|
|
241
|
+
| Property | Description |
|
|
242
|
+
|----------|-------------|
|
|
243
|
+
| `list.element` | Root DOM element |
|
|
244
|
+
| `list.items` | Current items (readonly) |
|
|
245
|
+
| `list.total` | Total item count |
|
|
674
246
|
|
|
675
|
-
###
|
|
247
|
+
### Lifecycle
|
|
676
248
|
|
|
677
249
|
```typescript
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// Measure items before creating list
|
|
681
|
-
const measuringDiv = document.createElement('div');
|
|
682
|
-
measuringDiv.style.cssText = 'position:absolute;visibility:hidden;width:400px';
|
|
683
|
-
document.body.appendChild(measuringDiv);
|
|
684
|
-
|
|
685
|
-
items.forEach(item => {
|
|
686
|
-
measuringDiv.innerHTML = renderItem(item);
|
|
687
|
-
item.measuredHeight = measuringDiv.offsetHeight;
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
document.body.removeChild(measuringDiv);
|
|
691
|
-
|
|
692
|
-
// Use measured heights
|
|
693
|
-
const list = vlist({
|
|
694
|
-
container: '#list',
|
|
695
|
-
items,
|
|
696
|
-
item: {
|
|
697
|
-
height: (i) => items[i].measuredHeight,
|
|
698
|
-
template: renderItem,
|
|
699
|
-
},
|
|
700
|
-
}).build();
|
|
250
|
+
list.destroy()
|
|
701
251
|
```
|
|
702
252
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
import { vlist, withGrid } from 'vlist';
|
|
707
|
-
|
|
708
|
-
let currentColumns = 4;
|
|
709
|
-
|
|
710
|
-
const list = vlist({
|
|
711
|
-
container: '#gallery',
|
|
712
|
-
items: photos,
|
|
713
|
-
item: {
|
|
714
|
-
height: 200,
|
|
715
|
-
template: renderPhoto,
|
|
716
|
-
},
|
|
717
|
-
})
|
|
718
|
-
.use(withGrid({ columns: currentColumns, gap: 16 }))
|
|
719
|
-
.build();
|
|
720
|
-
|
|
721
|
-
// Update grid on resize
|
|
722
|
-
window.addEventListener('resize', () => {
|
|
723
|
-
const width = container.clientWidth;
|
|
724
|
-
const newColumns = width > 1200 ? 6 : width > 800 ? 4 : 2;
|
|
725
|
-
if (newColumns !== currentColumns) {
|
|
726
|
-
currentColumns = newColumns;
|
|
727
|
-
list.updateGrid({ columns: newColumns });
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
```
|
|
253
|
+
## Plugin Configuration
|
|
731
254
|
|
|
732
|
-
|
|
255
|
+
Each plugin's config is fully typed â hover in your IDE for details.
|
|
733
256
|
|
|
734
257
|
```typescript
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const list = vlist({
|
|
745
|
-
container: '#gallery',
|
|
746
|
-
item: {
|
|
747
|
-
height: 200,
|
|
748
|
-
template: renderPhoto,
|
|
749
|
-
},
|
|
750
|
-
})
|
|
751
|
-
.use(withAsync({ adapter: photoAdapter }))
|
|
752
|
-
.use(withGrid({ columns: 4, gap: 16 }))
|
|
753
|
-
.use(withSections({
|
|
754
|
-
getGroupForIndex: (i) => items[i]?.category || 'Loading...',
|
|
755
|
-
headerHeight: 48,
|
|
756
|
-
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
757
|
-
}))
|
|
758
|
-
.use(withSelection({ mode: 'multiple' }))
|
|
759
|
-
.use(withScrollbar({ autoHide: true }))
|
|
760
|
-
.build();
|
|
258
|
+
withGrid({ columns: 4, gap: 16 })
|
|
259
|
+
withSections({ getGroupForIndex, headerHeight, headerTemplate, sticky?: true })
|
|
260
|
+
withSelection({ mode: 'single' | 'multiple', initial?: [...ids] })
|
|
261
|
+
withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
|
|
262
|
+
withScale() // no config â auto-activates at 16.7M px
|
|
263
|
+
withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
|
|
264
|
+
withPage() // no config â uses document scroll
|
|
265
|
+
withSnapshots() // included by default
|
|
761
266
|
```
|
|
762
267
|
|
|
763
|
-
|
|
268
|
+
Full configuration reference â **[vlist.dev](https://vlist.dev)**
|
|
764
269
|
|
|
765
270
|
## Framework Adapters
|
|
766
271
|
|
|
767
|
-
Framework
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
```bash
|
|
774
|
-
npm install @floor/vlist vlist-react
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
```tsx
|
|
778
|
-
import { useVList } from 'vlist-react';
|
|
779
|
-
import '@floor/vlist/styles';
|
|
780
|
-
|
|
781
|
-
function UserList({ users }) {
|
|
782
|
-
const { containerRef, instanceRef } = useVList({
|
|
783
|
-
item: {
|
|
784
|
-
height: 48,
|
|
785
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
786
|
-
},
|
|
787
|
-
items: users,
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
return <div ref={containerRef} style={{ height: 400 }} />;
|
|
791
|
-
}
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
**Documentation:** [github.com/floor/vlist-react](https://github.com/floor/vlist-react)
|
|
795
|
-
|
|
796
|
-
### Vue
|
|
797
|
-
|
|
798
|
-
**Package:** [`vlist-vue`](https://github.com/floor/vlist-vue) - 1.1 KB (0.6 KB gzipped)
|
|
799
|
-
|
|
800
|
-
```bash
|
|
801
|
-
npm install @floor/vlist vlist-vue
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
```vue
|
|
805
|
-
<script setup>
|
|
806
|
-
import { useVList } from 'vlist-vue';
|
|
807
|
-
import '@floor/vlist/styles';
|
|
808
|
-
|
|
809
|
-
const users = ref([...]);
|
|
810
|
-
|
|
811
|
-
const { containerRef, instance } = useVList({
|
|
812
|
-
item: {
|
|
813
|
-
height: 48,
|
|
814
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
815
|
-
},
|
|
816
|
-
items: users,
|
|
817
|
-
});
|
|
818
|
-
</script>
|
|
819
|
-
|
|
820
|
-
<template>
|
|
821
|
-
<div ref="containerRef" style="height: 400px" />
|
|
822
|
-
</template>
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
**Documentation:** [github.com/floor/vlist-vue](https://github.com/floor/vlist-vue)
|
|
826
|
-
|
|
827
|
-
### Svelte
|
|
828
|
-
|
|
829
|
-
**Package:** [`vlist-svelte`](https://github.com/floor/vlist-svelte) - 0.9 KB (0.5 KB gzipped)
|
|
272
|
+
| Framework | Package | Size |
|
|
273
|
+
|-----------|---------|------|
|
|
274
|
+
| React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
|
|
275
|
+
| Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
|
|
276
|
+
| Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
|
|
830
277
|
|
|
831
278
|
```bash
|
|
832
|
-
npm install @floor/vlist vlist-svelte
|
|
279
|
+
npm install @floor/vlist vlist-react # or vlist-vue / vlist-svelte
|
|
833
280
|
```
|
|
834
281
|
|
|
835
|
-
|
|
836
|
-
<script>
|
|
837
|
-
import { vlist } from 'vlist-svelte';
|
|
838
|
-
import '@floor/vlist/styles';
|
|
839
|
-
|
|
840
|
-
let users = [...];
|
|
841
|
-
let instance;
|
|
842
|
-
|
|
843
|
-
const config = {
|
|
844
|
-
item: {
|
|
845
|
-
height: 48,
|
|
846
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
847
|
-
},
|
|
848
|
-
items: users,
|
|
849
|
-
};
|
|
850
|
-
</script>
|
|
851
|
-
|
|
852
|
-
<div
|
|
853
|
-
use:vlist={{ config, onInstance: (i) => (instance = i) }}
|
|
854
|
-
style="height: 400px"
|
|
855
|
-
/>
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
**Documentation:** [github.com/floor/vlist-svelte](https://github.com/floor/vlist-svelte)
|
|
282
|
+
Each adapter README has setup examples and API docs.
|
|
859
283
|
|
|
860
284
|
## Styling
|
|
861
285
|
|
|
862
|
-
Import the base styles:
|
|
863
|
-
|
|
864
286
|
```typescript
|
|
865
|
-
import 'vlist/styles'
|
|
287
|
+
import '@floor/vlist/styles' // base styles (required)
|
|
288
|
+
import '@floor/vlist/styles/extras' // optional enhancements
|
|
866
289
|
```
|
|
867
290
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
```css
|
|
871
|
-
.vlist {
|
|
872
|
-
/* Container styles */
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
.vlist-viewport {
|
|
876
|
-
/* Scroll viewport */
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
.vlist-item {
|
|
880
|
-
/* Item wrapper - positioned absolutely */
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
.vlist-item--selected {
|
|
884
|
-
/* Selected state */
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.vlist-item--focused {
|
|
888
|
-
/* Focused state (keyboard navigation) */
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
.vlist-scrollbar {
|
|
892
|
-
/* Custom scrollbar track */
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
.vlist-scrollbar__thumb {
|
|
896
|
-
/* Scrollbar thumb/handle */
|
|
897
|
-
}
|
|
898
|
-
```
|
|
291
|
+
Override with your own CSS using the `.vlist`, `.vlist-item`, `.vlist-item--selected`, `.vlist-scrollbar` selectors. See [vlist.dev](https://vlist.dev) for theming examples.
|
|
899
292
|
|
|
900
293
|
## Performance
|
|
901
294
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
|
905
|
-
|
|
906
|
-
|
|
|
907
|
-
| +
|
|
908
|
-
|
|
|
909
|
-
| + Selection | 10.0 KB | + Item selection |
|
|
910
|
-
| + Async | 13.5 KB | + Data loading |
|
|
911
|
-
| + Scale | 9.9 KB | + 1M+ items |
|
|
912
|
-
| + All plugins | ~16 KB | Everything |
|
|
913
|
-
|
|
914
|
-
**Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
|
|
915
|
-
|
|
916
|
-
### Memory Efficiency
|
|
917
|
-
|
|
918
|
-
With 100,000 items at 48px each:
|
|
919
|
-
- **Total height:** 4,800,000 pixels
|
|
920
|
-
- **Visible items:** ~20 (depending on viewport height)
|
|
921
|
-
- **DOM nodes:** ~26 (visible + overscan)
|
|
922
|
-
- **Memory saved:** ~99.97% (26 DOM nodes vs 100,000)
|
|
295
|
+
| Configuration | Gzipped |
|
|
296
|
+
|---------------|---------|
|
|
297
|
+
| Base only | 7.7 KB |
|
|
298
|
+
| + Grid | 11.7 KB |
|
|
299
|
+
| + Sections | 12.3 KB |
|
|
300
|
+
| + Async | 13.5 KB |
|
|
301
|
+
| All plugins | ~16 KB |
|
|
923
302
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
See [benchmarks](https://vlist.dev/benchmarks/) for detailed performance comparisons.
|
|
927
|
-
|
|
928
|
-
## Browser Support
|
|
929
|
-
|
|
930
|
-
- Chrome/Edge: Latest 2 versions
|
|
931
|
-
- Firefox: Latest 2 versions
|
|
932
|
-
- Safari: Latest 2 versions
|
|
933
|
-
- iOS Safari: 12.4+
|
|
934
|
-
- Chrome Android: Latest
|
|
935
|
-
|
|
936
|
-
Relies on:
|
|
937
|
-
- `IntersectionObserver` (widely supported)
|
|
938
|
-
- `ResizeObserver` (polyfill available if needed)
|
|
939
|
-
- CSS `transform` (universal support)
|
|
303
|
+
With 100K items: **~26 DOM nodes** in the document (visible + overscan) instead of 100,000.
|
|
940
304
|
|
|
941
305
|
## TypeScript
|
|
942
306
|
|
|
943
|
-
Fully typed
|
|
307
|
+
Fully typed. Generic over your item type:
|
|
944
308
|
|
|
945
309
|
```typescript
|
|
946
|
-
import { vlist, withGrid, type VList
|
|
310
|
+
import { vlist, withGrid, type VList } from '@floor/vlist'
|
|
947
311
|
|
|
948
|
-
interface Photo {
|
|
949
|
-
id: number;
|
|
950
|
-
url: string;
|
|
951
|
-
title: string;
|
|
952
|
-
}
|
|
312
|
+
interface Photo { id: number; url: string; title: string }
|
|
953
313
|
|
|
954
|
-
const
|
|
314
|
+
const list: VList<Photo> = vlist<Photo>({
|
|
955
315
|
container: '#gallery',
|
|
956
316
|
items: photos,
|
|
957
317
|
item: {
|
|
958
318
|
height: 200,
|
|
959
|
-
template: (photo
|
|
319
|
+
template: (photo) => `<img src="${photo.url}" />`,
|
|
960
320
|
},
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
const list: VList<Photo> = vlist(config)
|
|
964
|
-
.use(withGrid({ columns: 4 }))
|
|
965
|
-
.build();
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
## Migration from Monolithic API
|
|
969
|
-
|
|
970
|
-
If you're using the old monolithic API:
|
|
971
|
-
|
|
972
|
-
### Before (Monolithic - Deprecated)
|
|
973
|
-
|
|
974
|
-
```typescript
|
|
975
|
-
import { createVList } from 'vlist';
|
|
976
|
-
|
|
977
|
-
const list = createVList({
|
|
978
|
-
container: '#app',
|
|
979
|
-
items: data,
|
|
980
|
-
grid: { columns: 4 },
|
|
981
|
-
groups: { ... },
|
|
982
|
-
selection: { mode: 'single' },
|
|
983
|
-
});
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
### After (Builder - Recommended)
|
|
987
|
-
|
|
988
|
-
```typescript
|
|
989
|
-
import { vlist, withGrid, withSections, withSelection } from 'vlist';
|
|
990
|
-
|
|
991
|
-
const list = vlist({
|
|
992
|
-
container: '#app',
|
|
993
|
-
items: data,
|
|
994
321
|
})
|
|
995
322
|
.use(withGrid({ columns: 4 }))
|
|
996
|
-
.
|
|
997
|
-
.use(withSelection({ mode: 'single' }))
|
|
998
|
-
.build();
|
|
323
|
+
.build()
|
|
999
324
|
```
|
|
1000
325
|
|
|
1001
|
-
**Benefits:**
|
|
1002
|
-
- 2-3x smaller bundles (20 KB â 8-12 KB gzipped)
|
|
1003
|
-
- Explicit about what's included
|
|
1004
|
-
- Better tree-shaking
|
|
1005
|
-
- Easier to understand and debug
|
|
1006
|
-
|
|
1007
|
-
## Plugin Naming Changes
|
|
1008
|
-
|
|
1009
|
-
If you're upgrading from an earlier version:
|
|
1010
|
-
|
|
1011
|
-
| Old Name | New Name | Import |
|
|
1012
|
-
|----------|----------|--------|
|
|
1013
|
-
| `withCompression()` | `withScale()` | `import { withScale } from 'vlist'` |
|
|
1014
|
-
| `withData()` | `withAsync()` | `import { withAsync } from 'vlist'` |
|
|
1015
|
-
| `withWindow()` | `withPage()` | `import { withPage } from 'vlist'` |
|
|
1016
|
-
| `withGroups()` | `withSections()` | `import { withSections } from 'vlist'` |
|
|
1017
|
-
| `withScroll()` | `withScrollbar()` | `import { withScrollbar } from 'vlist'` |
|
|
1018
|
-
|
|
1019
326
|
## Contributing
|
|
1020
327
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
1. Fork the repository
|
|
1024
|
-
2. Create a feature branch
|
|
1025
|
-
3. Make your changes
|
|
1026
|
-
4. Add/update tests
|
|
1027
|
-
5. Submit a pull request
|
|
328
|
+
1. Fork â branch â make changes â add tests â pull request
|
|
329
|
+
2. Run `bun test` (1739 tests) and `bun run build` before submitting
|
|
1028
330
|
|
|
1029
331
|
## License
|
|
1030
332
|
|
|
1031
|
-
MIT
|
|
333
|
+
[MIT](LICENSE)
|
|
1032
334
|
|
|
1033
335
|
## Links
|
|
1034
336
|
|
|
1035
|
-
- **
|
|
337
|
+
- **Docs & Examples:** [vlist.dev](https://vlist.dev)
|
|
1036
338
|
- **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
|
|
1037
339
|
- **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
|
|
1038
340
|
- **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
|
|
1039
341
|
|
|
1040
342
|
---
|
|
1041
343
|
|
|
1042
|
-
|
|
344
|
+
Built by [Floor IO](https://floor.io)
|