@floor/vlist 0.5.7 → 0.6.0

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.
Files changed (108) hide show
  1. package/README.md +793 -592
  2. package/dist/async/index.js +1 -0
  3. package/dist/builder/context.d.ts +3 -3
  4. package/dist/builder/context.d.ts.map +1 -1
  5. package/dist/builder/core.d.ts +1 -1
  6. package/dist/builder/core.d.ts.map +1 -1
  7. package/dist/builder/index.js +1 -250
  8. package/dist/builder/types.d.ts +3 -3
  9. package/dist/builder/types.d.ts.map +1 -1
  10. package/dist/compression/index.js +1 -104
  11. package/dist/core/core.js +1 -0
  12. package/dist/core/full.d.ts +22 -0
  13. package/dist/core/full.d.ts.map +1 -0
  14. package/dist/core/index.js +1 -133
  15. package/dist/core/lite.d.ts +129 -0
  16. package/dist/core/lite.d.ts.map +1 -0
  17. package/dist/core/minimal.d.ts +104 -0
  18. package/dist/core/minimal.d.ts.map +1 -0
  19. package/dist/core-light.js +1 -68
  20. package/dist/data/index.js +1 -233
  21. package/dist/features/async/index.d.ts +9 -0
  22. package/dist/features/async/index.d.ts.map +1 -0
  23. package/dist/features/async/manager.d.ts +103 -0
  24. package/dist/features/async/manager.d.ts.map +1 -0
  25. package/dist/features/async/placeholder.d.ts +62 -0
  26. package/dist/features/async/placeholder.d.ts.map +1 -0
  27. package/dist/features/async/plugin.d.ts +60 -0
  28. package/dist/features/async/plugin.d.ts.map +1 -0
  29. package/dist/features/async/sparse.d.ts +91 -0
  30. package/dist/features/async/sparse.d.ts.map +1 -0
  31. package/dist/features/grid/index.d.ts +9 -0
  32. package/dist/features/grid/index.d.ts.map +1 -0
  33. package/dist/features/grid/layout.d.ts +29 -0
  34. package/dist/features/grid/layout.d.ts.map +1 -0
  35. package/dist/features/grid/plugin.d.ts +48 -0
  36. package/dist/features/grid/plugin.d.ts.map +1 -0
  37. package/dist/features/grid/renderer.d.ts +55 -0
  38. package/dist/features/grid/renderer.d.ts.map +1 -0
  39. package/dist/features/grid/types.d.ts +71 -0
  40. package/dist/features/grid/types.d.ts.map +1 -0
  41. package/dist/features/page/index.d.ts +8 -0
  42. package/dist/features/page/index.d.ts.map +1 -0
  43. package/dist/features/page/plugin.d.ts +53 -0
  44. package/dist/features/page/plugin.d.ts.map +1 -0
  45. package/dist/features/scale/index.d.ts +10 -0
  46. package/dist/features/scale/index.d.ts.map +1 -0
  47. package/dist/features/scale/plugin.d.ts +42 -0
  48. package/dist/features/scale/plugin.d.ts.map +1 -0
  49. package/dist/features/scrollbar/controller.d.ts +121 -0
  50. package/dist/features/scrollbar/controller.d.ts.map +1 -0
  51. package/dist/features/scrollbar/index.d.ts +8 -0
  52. package/dist/features/scrollbar/index.d.ts.map +1 -0
  53. package/dist/features/scrollbar/plugin.d.ts +60 -0
  54. package/dist/features/scrollbar/plugin.d.ts.map +1 -0
  55. package/dist/features/scrollbar/scrollbar.d.ts +73 -0
  56. package/dist/features/scrollbar/scrollbar.d.ts.map +1 -0
  57. package/dist/features/sections/index.d.ts +10 -0
  58. package/dist/features/sections/index.d.ts.map +1 -0
  59. package/dist/features/sections/layout.d.ts +46 -0
  60. package/dist/features/sections/layout.d.ts.map +1 -0
  61. package/dist/features/sections/plugin.d.ts +64 -0
  62. package/dist/features/sections/plugin.d.ts.map +1 -0
  63. package/dist/features/sections/sticky.d.ts +33 -0
  64. package/dist/features/sections/sticky.d.ts.map +1 -0
  65. package/dist/features/sections/types.d.ts +86 -0
  66. package/dist/features/sections/types.d.ts.map +1 -0
  67. package/dist/features/selection/index.d.ts +7 -0
  68. package/dist/features/selection/index.d.ts.map +1 -0
  69. package/dist/features/selection/plugin.d.ts +44 -0
  70. package/dist/features/selection/plugin.d.ts.map +1 -0
  71. package/dist/features/selection/state.d.ts +102 -0
  72. package/dist/features/selection/state.d.ts.map +1 -0
  73. package/dist/features/snapshots/index.d.ts +8 -0
  74. package/dist/features/snapshots/index.d.ts.map +1 -0
  75. package/dist/features/snapshots/plugin.d.ts +44 -0
  76. package/dist/features/snapshots/plugin.d.ts.map +1 -0
  77. package/dist/grid/index.js +1 -198
  78. package/dist/groups/index.js +1 -204
  79. package/dist/index.d.ts +17 -8
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1 -1129
  82. package/dist/page/index.js +1 -0
  83. package/dist/plugins/groups/plugin.d.ts +3 -2
  84. package/dist/plugins/groups/plugin.d.ts.map +1 -1
  85. package/dist/react/index.js +1 -1024
  86. package/dist/react/react.js +1 -0
  87. package/dist/rendering/heights.d.ts +63 -0
  88. package/dist/rendering/heights.d.ts.map +1 -0
  89. package/dist/rendering/index.d.ts +9 -0
  90. package/dist/rendering/index.d.ts.map +1 -0
  91. package/dist/rendering/renderer.d.ts +103 -0
  92. package/dist/rendering/renderer.d.ts.map +1 -0
  93. package/dist/rendering/scale.d.ts +116 -0
  94. package/dist/rendering/scale.d.ts.map +1 -0
  95. package/dist/rendering/viewport.d.ts +139 -0
  96. package/dist/rendering/viewport.d.ts.map +1 -0
  97. package/dist/scale/index.js +1 -0
  98. package/dist/scroll/index.js +1 -116
  99. package/dist/scrollbar/index.js +1 -0
  100. package/dist/sections/index.js +1 -0
  101. package/dist/selection/index.js +1 -96
  102. package/dist/snapshots/index.js +1 -21
  103. package/dist/svelte/index.js +1 -1012
  104. package/dist/svelte/svelte.js +1 -0
  105. package/dist/vue/index.js +1 -1018
  106. package/dist/vue/vue.js +1 -0
  107. package/dist/window/index.js +1 -18
  108. package/package.json +1 -1
package/README.md CHANGED
@@ -1,49 +1,38 @@
1
1
  # vlist
2
2
 
3
- Lightweight, high-performance virtual list with zero dependencies.
3
+ Lightweight, high-performance virtual list with zero dependencies and optimal tree-shaking.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40floor%2Fvlist.svg)](https://www.npmjs.com/package/@floor/vlist)
6
6
  [![bundle size](https://img.shields.io/bundlephobia/minzip/@floor/vlist)](https://bundlephobia.com/package/@floor/vlist)
7
- [![tests](https://img.shields.io/badge/tests-1730%20passing-brightgreen)](https://github.com/floor/vlist)
7
+ [![tests](https://img.shields.io/badge/tests-1739%20passing-brightgreen)](https://github.com/floor/vlist)
8
8
  [![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)
9
9
 
10
10
  ## Features
11
11
 
12
12
  - 🪶 **Zero dependencies** - No external libraries required
13
- - ⚡ **Blazing fast** - Only renders visible items with element pooling
14
- - 🎯 **Simple API** - Easy to use with TypeScript support
15
- - 📐 **Grid layout** - 2D virtualized grid with configurable columns and gap
16
- - 📏 **Variable heights** - Fixed or per-item height via `(index) => number`
17
- - ↔️ **Horizontal scrolling** - Horizontal lists and carousels with `direction: 'horizontal'`
18
- - 📜 **Infinite scroll** - Built-in async adapter support
13
+ - ⚡ **Tiny bundle** - 8-12 KB gzipped (vs 20+ KB for traditional virtual lists)
14
+ - 🌲 **Perfect tree-shaking** - Pay only for features you use
15
+ - 🎯 **Builder API** - Explicit, composable plugin system
16
+ - 📐 **Grid layout** - 2D virtualized grid with configurable columns
17
+ - 📏 **Variable heights** - Fixed or per-item height calculation
18
+ - ↔️ **Horizontal scrolling** - Horizontal lists and carousels
19
+ - 📜 **Async loading** - Built-in lazy loading with adapters
19
20
  - ✅ **Selection** - Single and multiple selection modes
20
21
  - 📌 **Sticky headers** - Grouped lists with sticky section headers
21
- - 🪟 **Window scrolling** - Document-level scrolling with `scrollElement: window`
22
- - 🔄 **Wrap navigation** - Circular scrolling for wizards and carousels
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
23
26
  - 🎨 **Customizable** - Beautiful, customizable styles
24
- - ♿ **Accessible** - WAI-ARIA listbox pattern, `aria-setsize`/`aria-posinset`, `aria-activedescendant`, live region, keyboard navigation
25
- - 🌊 **Smooth scrolling** - Animated `scrollToIndex` / `scrollToItem`
26
- - 💾 **Scroll save/restore** - `getScrollSnapshot()` / `restoreScroll()` for SPA navigation
27
- - 💬 **Reverse mode** - Chat UI support with auto-scroll, scroll-preserving prepend
28
- - 🔌 **Framework adapters** - Thin wrappers for React, Vue, and Svelte (<1 KB each)
29
- - 🌲 **Tree-shakeable** - Sub-module imports for smaller bundles
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
30
 
31
31
  ## Sandbox & Documentation
32
32
 
33
- Interactive examples and documentation are available at **[vlist.dev](https://vlist.dev)**.
33
+ Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
34
34
 
35
- **14 examples** across 7 categories — many with multi-framework implementations (JavaScript, React, Svelte, Vue):
36
-
37
- | Category | Examples |
38
- |----------|----------|
39
- | **Getting Started** | [Basic](https://vlist.dev/sandbox/basic/) • [Controls](https://vlist.dev/sandbox/controls/) |
40
- | **Core (Lightweight)** | [Basic Core](https://vlist.dev/sandbox/core/basic/) — 7.8KB, 83% smaller |
41
- | **Grid Plugin** | [Photo Album](https://vlist.dev/sandbox/grid/photo-album/) • [File Browser](https://vlist.dev/sandbox/grid/file-browser/) |
42
- | **Data Plugin** | [Large List](https://vlist.dev/sandbox/data/large-list/) (100K–5M items) • [Velocity Loading](https://vlist.dev/sandbox/data/velocity-loading/) |
43
- | **Horizontal** | [Basic Horizontal](https://vlist.dev/sandbox/horizontal/basic/) — 10K card carousel |
44
- | **Groups Plugin** | [Sticky Headers](https://vlist.dev/sandbox/groups/sticky-headers/) — A–Z contact list |
45
- | **Other Plugins** | [Scroll Restore](https://vlist.dev/sandbox/scroll-restore/) • [Window Scroll](https://vlist.dev/sandbox/window-scroll/) |
46
- | **Advanced** | [Variable Heights](https://vlist.dev/sandbox/variable-heights/) • [Reverse Chat](https://vlist.dev/sandbox/reverse-chat/) • [Wizard Nav](https://vlist.dev/sandbox/wizard-nav/) |
35
+ **30+ examples** with multi-framework implementations (JavaScript, React, Vue, Svelte)
47
36
 
48
37
  ## Installation
49
38
 
@@ -51,796 +40,1008 @@ Interactive examples and documentation are available at **[vlist.dev](https://vl
51
40
  npm install @floor/vlist
52
41
  ```
53
42
 
54
- > **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to the simpler `vlist` name.
55
-
56
- ### Sub-module Imports
43
+ > **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
57
44
 
58
- For smaller bundles, import only what you need:
45
+ ## Quick Start
59
46
 
60
47
  ```typescript
61
- import { createVList } from '@floor/vlist' // full library (70 KB / 23 KB gzip)
62
- import { createVList } from '@floor/vlist/core' // lightweight core (8 KB / 3 KB gzip)
63
- import { createVList } from '@floor/vlist/core-light' // ultra-minimal (5 KB / 2 KB gzip)
64
- import { createVList } from '@floor/vlist/builder' // declarative API (17 KB / 6 KB gzip)
65
- import { createGridLayout } from '@floor/vlist/grid' // grid layout utilities only
66
- import { createSparseStorage } from '@floor/vlist/data' // data utilities only
67
- import { getCompressionInfo } from '@floor/vlist/compression' // compression utilities only
68
- import { createSelectionState } from '@floor/vlist/selection' // selection utilities only
69
- import { createScrollController } from '@floor/vlist/scroll' // scroll utilities only
70
- import { createGroupLayout } from '@floor/vlist/groups' // group/sticky header utilities only
48
+ import { vlist } from 'vlist';
49
+ import 'vlist/styles';
50
+
51
+ const list = vlist({
52
+ container: '#my-list',
53
+ items: [
54
+ { id: 1, name: 'Alice' },
55
+ { id: 2, name: 'Bob' },
56
+ { id: 3, name: 'Charlie' },
57
+ ],
58
+ item: {
59
+ height: 48,
60
+ template: (item) => `<div>${item.name}</div>`,
61
+ },
62
+ }).build();
63
+
64
+ // API methods
65
+ list.scrollToIndex(10);
66
+ list.setItems(newItems);
67
+ list.on('item:click', ({ item }) => console.log(item));
71
68
  ```
72
69
 
73
- | Import | Minified | Gzipped | Description |
74
- |--------|----------|---------|-------------|
75
- | `@floor/vlist` | 70 KB | 23 KB | All features (plugins + framework adapters) |
76
- | **`@floor/vlist/core`** | **8 KB** | **3 KB** | **Core virtual list — 88% smaller** |
77
- | **`@floor/vlist/core-light`** | **5 KB** | **2 KB** | **Ultra-minimal — 93% smaller** |
78
- | `@floor/vlist/builder` | 17 KB | 6 KB | Declarative API with chaining |
79
- | `@floor/vlist/data` | 12 KB | 5 KB | Sparse storage, placeholders, data manager |
80
- | `@floor/vlist/scroll` | 9 KB | 3 KB | Scroll controller + custom scrollbar |
81
- | `@floor/vlist/grid` | 10 KB | 4 KB | Grid layout + 2D renderer |
82
- | `@floor/vlist/groups` | 11 KB | 5 KB | Group layout + sticky headers |
83
- | `@floor/vlist/compression` | 8 KB | 3 KB | Large-list compression utilities |
84
- | `@floor/vlist/selection` | 6 KB | 2 KB | Selection state management |
70
+ **Bundle:** ~8 KB gzipped
85
71
 
86
- ### Framework Adapters
72
+ ## Builder Pattern
87
73
 
88
- Thin wrappers for React, Vue, and Svelte each under 1 KB:
74
+ VList uses a composable builder pattern. Start with the base, add only the features you need:
89
75
 
90
76
  ```typescript
91
- import { useVList } from '@floor/vlist/react' // React hook (62 KB / 21 KB gzip)
92
- import { useVList } from '@floor/vlist/vue' // Vue 3 composable (62 KB / 21 KB gzip)
93
- import { vlist } from '@floor/vlist/svelte' // Svelte action (62 KB / 20 KB gzip)
77
+ import { vlist, withGrid, withSections, withSelection } from 'vlist';
78
+
79
+ const list = vlist({
80
+ container: '#app',
81
+ items: photos,
82
+ item: {
83
+ height: 200,
84
+ template: renderPhoto,
85
+ },
86
+ })
87
+ .use(withGrid({ columns: 4, gap: 16 }))
88
+ .use(withSections({
89
+ getGroupForIndex: (i) => photos[i].category,
90
+ headerHeight: 40,
91
+ headerTemplate: (cat) => `<h2>${cat}</h2>`,
92
+ }))
93
+ .use(withSelection({ mode: 'multiple' }))
94
+ .build();
94
95
  ```
95
96
 
96
- | Import | Minified | Gzipped | Description |
97
- |--------|----------|---------|-------------|
98
- | `@floor/vlist/react` | 0.7 KB | 0.4 KB | `useVList` hook + `useVListEvent` |
99
- | `@floor/vlist/vue` | 0.5 KB | 0.4 KB | `useVList` composable + `useVListEvent` |
100
- | `@floor/vlist/svelte` | 0.3 KB | 0.2 KB | `vlist` action + `onVListEvent` |
97
+ **Bundle:** ~12 KB gzipped (only includes used plugins)
101
98
 
102
- Adapters manage the vlist lifecycle (create on mount, destroy on unmount) and sync items reactively. See [Framework Adapters](#framework-adapters) for full examples.
99
+ ### Available Plugins
103
100
 
104
- ## Quick Start
101
+ | Plugin | Cost | Description |
102
+ |--------|------|-------------|
103
+ | **Base** | 7.7 KB gzip | Core virtualization, no plugins |
104
+ | `withGrid()` | +4.0 KB | 2D grid layout |
105
+ | `withSections()` | +4.6 KB | Grouped lists with sticky/inline headers |
106
+ | `withAsync()` | +5.3 KB | Async data loading with adapters |
107
+ | `withSelection()` | +2.3 KB | Single/multiple item selection |
108
+ | `withScale()` | +2.2 KB | Handle 1M+ items with compression |
109
+ | `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
110
+ | `withPage()` | +0.9 KB | Document-level scrolling |
111
+ | `withSnapshots()` | Included | Scroll save/restore |
112
+
113
+ **Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
114
+
115
+ ## Examples
116
+
117
+ ### Simple List (No Plugins)
105
118
 
106
119
  ```typescript
107
- import { createVList } from '@floor/vlist';
108
- import '@floor/vlist/styles';
120
+ import { vlist } from 'vlist';
109
121
 
110
- const list = createVList({
111
- container: '#my-list',
112
- ariaLabel: 'Contact list',
122
+ const list = vlist({
123
+ container: '#list',
124
+ items: users,
113
125
  item: {
114
- height: 48,
115
- template: (item) => `
116
- <div class="item-content">
117
- <img src="${item.avatar}" class="avatar" />
118
- <span>${item.name}</span>
126
+ height: 64,
127
+ template: (user) => `
128
+ <div class="user">
129
+ <img src="${user.avatar}" />
130
+ <span>${user.name}</span>
119
131
  </div>
120
132
  `,
121
133
  },
122
- items: [
123
- { id: 1, name: 'Alice', avatar: '/avatars/alice.jpg' },
124
- { id: 2, name: 'Bob', avatar: '/avatars/bob.jpg' },
125
- // ... more items
126
- ],
127
- });
134
+ }).build();
128
135
  ```
129
136
 
130
- ### Lightweight Core (7.8 KB)
137
+ **Bundle:** 8.2 KB gzipped
131
138
 
132
- If you don't need selection, groups, grid, compression, custom scrollbar, or async data adapters, use the lightweight core for an **83% smaller bundle**:
139
+ ### Grid Layout
133
140
 
134
141
  ```typescript
135
- import { createVList } from '@floor/vlist/core';
136
- import '@floor/vlist/styles';
142
+ import { vlist, withGrid, withScrollbar } from 'vlist';
137
143
 
138
- const list = createVList({
139
- container: '#my-list',
144
+ const gallery = vlist({
145
+ container: '#gallery',
146
+ items: photos,
140
147
  item: {
141
- height: 48,
142
- template: (item) => `<div>${item.name}</div>`,
148
+ height: 200,
149
+ template: (photo) => `
150
+ <div class="card">
151
+ <img src="${photo.url}" />
152
+ <span>${photo.title}</span>
153
+ </div>
154
+ `,
143
155
  },
144
- items: myItems,
145
- });
146
-
147
- // Same core API: setItems, appendItems, scrollToIndex, events, etc.
148
- list.on('item:click', ({ item }) => console.log(item));
149
- list.scrollToIndex(50, { behavior: 'smooth' });
156
+ })
157
+ .use(withGrid({ columns: 4, gap: 16 }))
158
+ .use(withScrollbar({ autoHide: true }))
159
+ .build();
150
160
  ```
151
161
 
152
- 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.
162
+ **Bundle:** 11.7 KB gzipped
153
163
 
154
- ## Configuration
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)
155
167
 
156
168
  ```typescript
157
- interface VListConfig<T> {
158
- // Required
159
- container: HTMLElement | string; // Container element or selector
169
+ import { vlist, withSections } from 'vlist';
170
+
171
+ const contacts = vlist({
172
+ container: '#contacts',
173
+ items: sortedContacts, // Must be pre-sorted by group
160
174
  item: {
161
- height: number | ((index: number) => number); // Fixed or variable height
162
- template: ItemTemplate<T>; // Render function for each item
163
- };
175
+ height: 56,
176
+ template: (contact) => `<div>${contact.name}</div>`,
177
+ },
178
+ })
179
+ .use(withSections({
180
+ getGroupForIndex: (i) => contacts[i].lastName[0].toUpperCase(),
181
+ headerHeight: 36,
182
+ headerTemplate: (letter) => `<div class="header">${letter}</div>`,
183
+ sticky: true, // Headers stick to top (Telegram style)
184
+ }))
185
+ .build();
186
+ ```
164
187
 
165
- // Layout
166
- layout?: 'list' | 'grid'; // Layout mode (default: 'list')
167
- grid?: { // Grid config (required when layout: 'grid')
168
- columns: number; // Number of columns
169
- gap?: number; // Gap between items in px (default: 0)
170
- };
188
+ **Bundle:** 12.3 KB gzipped
171
189
 
172
- // Data
173
- items?: T[]; // Static items array
174
- adapter?: VListAdapter<T>; // Async data adapter
190
+ Set `sticky: false` for inline headers (iMessage/WhatsApp style).
175
191
 
176
- // Scrolling
177
- overscan?: number; // Extra items to render (default: 3)
178
- scroll?: {
179
- wheel?: boolean; // Enable mouse wheel (default: true)
180
- wrap?: boolean; // Wrap around at boundaries (default: false)
181
- scrollbar?: 'native' | 'none' // Scrollbar mode (default: custom)
182
- | ScrollbarOptions; // or { autoHide, autoHideDelay, minThumbSize }
183
- element?: Window; // Window scrolling mode
184
- idleTimeout?: number; // Scroll idle detection in ms (default: 150)
185
- };
192
+ ### Chat UI (Reverse + Sections)
193
+
194
+ ```typescript
195
+ import { vlist, withSections } from 'vlist';
186
196
 
187
- // Features
188
- selection?: SelectionConfig; // Selection configuration
189
- groups?: GroupsConfig; // Sticky headers / grouped lists
190
- loading?: LoadingConfig; // Velocity-based loading thresholds
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();
191
216
 
192
- // Chat UI
193
- reverse?: boolean; // Reverse mode (start at bottom, auto-scroll)
217
+ // New messages - auto-scrolls to bottom
218
+ chat.appendItems([newMessage]);
194
219
 
195
- // Appearance
196
- classPrefix?: string; // CSS class prefix (default: 'vlist')
197
- ariaLabel?: string; // Accessible label for the listbox
198
- }
220
+ // Load history - preserves scroll position
221
+ chat.prependItems(olderMessages);
199
222
  ```
200
223
 
201
- ## Examples
224
+ **Bundle:** 11.9 KB gzipped
202
225
 
203
- ### Grid Layout
226
+ Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
227
+
228
+ ### Large Datasets (1M+ Items)
204
229
 
205
230
  ```typescript
206
- const grid = createVList({
207
- container: '#gallery',
208
- layout: 'grid',
209
- grid: {
210
- columns: 4,
211
- gap: 8, // 8px gap between columns AND rows
212
- },
231
+ import { vlist, withScale, withScrollbar } from 'vlist';
232
+
233
+ const bigList = vlist({
234
+ container: '#big-list',
235
+ items: generateItems(5_000_000),
213
236
  item: {
214
- height: 200,
215
- template: (item) => `
216
- <div class="card">
217
- <img src="${item.thumbnail}" />
218
- <span>${item.title}</span>
219
- </div>
220
- `,
237
+ height: 48,
238
+ template: (item) => `<div>#${item.id}: ${item.name}</div>`,
221
239
  },
222
- items: photos,
223
- });
240
+ })
241
+ .use(withScale()) // Auto-activates when height > 16.7M pixels
242
+ .use(withScrollbar({ autoHide: true }))
243
+ .build();
224
244
  ```
225
245
 
226
- 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.
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.
227
249
 
228
- ### Variable Heights
250
+ ### Async Loading with Pagination
229
251
 
230
252
  ```typescript
231
- const list = createVList({
232
- container: '#messages',
253
+ import { vlist, withAsync } from 'vlist';
254
+
255
+ const list = vlist({
256
+ container: '#list',
233
257
  item: {
234
- height: (index) => messages[index].type === 'header' ? 32 : 64,
235
- template: (item) => `<div class="message">${item.text}</div>`,
258
+ height: 64,
259
+ template: (item) => {
260
+ if (!item) return `<div class="loading">Loading...</div>`;
261
+ return `<div>${item.name}</div>`;
262
+ },
236
263
  },
237
- items: messages,
238
- });
264
+ })
265
+ .use(withAsync({
266
+ adapter: {
267
+ read: async ({ offset, limit }) => {
268
+ const response = await fetch(`/api/users?offset=${offset}&limit=${limit}`);
269
+ const data = await response.json();
270
+ return {
271
+ items: data.items,
272
+ total: data.total,
273
+ hasMore: data.hasMore,
274
+ };
275
+ },
276
+ },
277
+ loading: {
278
+ cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
279
+ },
280
+ }))
281
+ .build();
239
282
  ```
240
283
 
241
- Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search for index-at-offset.
284
+ **Bundle:** 13.5 KB gzipped
285
+
286
+ The async plugin shows placeholders for unloaded items and fetches data as you scroll. Velocity-aware loading cancels requests when scrolling fast.
242
287
 
243
- ### Sticky Headers
288
+ ### Page-Level Scrolling
244
289
 
245
290
  ```typescript
246
- const list = createVList({
247
- container: '#contacts',
291
+ import { vlist, withPage } from 'vlist';
292
+
293
+ const list = vlist({
294
+ container: '#list',
295
+ items: articles,
248
296
  item: {
249
- height: 56,
250
- template: (item) => `<div>${item.name}</div>`,
251
- },
252
- items: contacts, // Must be pre-sorted by group
253
- groups: {
254
- getGroupForIndex: (index) => contacts[index].lastName[0],
255
- headerHeight: 36,
256
- headerTemplate: (group) => `<div class="section-header">${group}</div>`,
257
- sticky: true, // Headers stick to the top (default: true)
297
+ height: 200,
298
+ template: (article) => `<article>...</article>`,
258
299
  },
259
- });
300
+ })
301
+ .use(withPage()) // Uses document scroll instead of container
302
+ .build();
260
303
  ```
261
304
 
262
- ### Window Scrolling
305
+ **Bundle:** 8.6 KB gzipped
306
+
307
+ Perfect for blog posts, infinite scroll feeds, and full-page lists.
308
+
309
+ ### Horizontal Carousel
263
310
 
264
311
  ```typescript
265
- const list = createVList({
266
- container: '#my-list',
267
- scroll: { element: window }, // Use the browser's native scrollbar
312
+ import { vlist } from 'vlist';
313
+
314
+ const carousel = vlist({
315
+ container: '#carousel',
316
+ direction: 'horizontal',
317
+ items: cards,
268
318
  item: {
269
- height: 48,
270
- template: (item) => `<div>${item.name}</div>`,
319
+ width: 300, // Required for horizontal
320
+ height: 400, // Optional (can use CSS)
321
+ template: (card) => `<div class="card">...</div>`,
271
322
  },
272
- items: myItems,
273
- });
323
+ scroll: {
324
+ wrap: true, // Circular scrolling
325
+ },
326
+ }).build();
274
327
  ```
275
328
 
276
- ### Wizard / Carousel (Wrap Navigation)
329
+ **Bundle:** 8.6 KB gzipped
330
+
331
+ ### Selection & Navigation
277
332
 
278
333
  ```typescript
279
- const wizard = createVList({
280
- container: '#wizard',
281
- scroll: { wheel: false, scrollbar: 'none', wrap: true },
334
+ import { vlist, withSelection } from 'vlist';
335
+
336
+ const list = vlist({
337
+ container: '#list',
338
+ items: users,
282
339
  item: {
283
- height: 400,
284
- template: (step) => `<div class="step">${step.content}</div>`,
340
+ height: 48,
341
+ template: (user, index, { selected }) => {
342
+ const cls = selected ? 'item--selected' : '';
343
+ return `<div class="${cls}">${user.name}</div>`;
344
+ },
285
345
  },
286
- items: steps,
287
- });
288
-
289
- let current = 0;
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
+ ```
290
360
 
291
- // No boundary checks needed — wrap handles it
292
- btnNext.addEventListener('click', () => {
293
- current++;
294
- wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
295
- });
361
+ **Bundle:** 10.0 KB gzipped
296
362
 
297
- btnPrev.addEventListener('click', () => {
298
- current--;
299
- wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
300
- });
301
- ```
363
+ Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
302
364
 
303
- ### Reverse Mode (Chat UI)
365
+ ### Variable Heights (Chat Messages)
304
366
 
305
367
  ```typescript
306
- const chat = createVList({
368
+ import { vlist } from 'vlist';
369
+
370
+ const list = vlist({
307
371
  container: '#messages',
308
- reverse: true,
372
+ items: messages,
309
373
  item: {
310
- height: (index) => messages[index].type === 'image' ? 200 : 60,
374
+ height: (index) => {
375
+ // Heights computed from actual DOM measurements
376
+ return messages[index].measuredHeight || 60;
377
+ },
311
378
  template: (msg) => `
312
- <div class="bubble bubble--${msg.sender}">
313
- <span class="sender">${msg.sender}</span>
314
- <p>${msg.text}</p>
379
+ <div class="message">
380
+ <div class="author">${msg.user}</div>
381
+ <div class="text">${msg.text}</div>
315
382
  </div>
316
383
  `,
317
384
  },
318
- items: messages, // Chronological order (oldest first)
319
- });
385
+ }).build();
386
+ ```
320
387
 
321
- // New message arrives — auto-scrolls to bottom if user was at bottom
322
- chat.appendItems([newMessage]);
388
+ **Bundle:** 10.9 KB gzipped
323
389
 
324
- // Load older messages scroll position preserved (no jump)
325
- chat.prependItems(olderMessages);
326
- ```
390
+ Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
327
391
 
328
- 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`.
392
+ ## API Reference
329
393
 
330
- ### With Selection
394
+ ### Core Methods
331
395
 
332
396
  ```typescript
333
- const list = createVList({
334
- container: '#my-list',
335
- item: {
336
- height: 56,
337
- template: (item, index, { selected }) => `
338
- <div class="item-content ${selected ? 'selected' : ''}">
339
- <span>${item.name}</span>
340
- ${selected ? '✓' : ''}
341
- </div>
342
- `,
343
- },
344
- items: users,
345
- selection: {
346
- mode: 'multiple', // 'none' | 'single' | 'multiple'
347
- initial: [1, 2], // Initially selected IDs
348
- },
349
- });
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
+ ```
350
433
 
351
- // Listen for selection changes
352
- list.on('selection:change', ({ selected, items }) => {
353
- console.log('Selected:', selected);
354
- });
434
+ ### Selection Methods (with `withSelection()`)
355
435
 
356
- // Programmatic selection
357
- list.select(5);
358
- list.deselect(1);
359
- list.selectAll();
360
- list.clearSelection();
436
+ ```typescript
437
+ list.selectItem(id: string | number): void
438
+ list.deselectItem(id: string | number): void
439
+ list.toggleSelection(id: string | number): void
440
+ list.selectAll(): void
441
+ list.clearSelection(): void
442
+ list.getSelectedIds(): Array<string | number>
443
+ list.getSelectedItems(): T[]
444
+ list.setSelectionMode(mode: 'none' | 'single' | 'multiple'): void
361
445
  ```
362
446
 
363
- ### With Infinite Scroll
447
+ ### Grid Methods (with `withGrid()`)
364
448
 
365
449
  ```typescript
366
- const list = createVList({
367
- container: '#my-list',
368
- item: {
369
- height: 64,
370
- template: (item) => `<div>${item.title}</div>`,
371
- },
372
- adapter: {
373
- read: async ({ offset, limit }) => {
374
- const response = await fetch(
375
- `/api/items?offset=${offset}&limit=${limit}`
376
- );
377
- const data = await response.json();
378
-
379
- return {
380
- items: data.items,
381
- total: data.total,
382
- hasMore: data.hasMore,
383
- };
384
- },
385
- },
386
- });
450
+ list.updateGrid(config: { columns?: number, gap?: number }): void
451
+ ```
387
452
 
388
- // Listen for loading events
389
- list.on('load:start', ({ offset, limit }) => {
390
- console.log('Loading...', offset, limit);
391
- });
453
+ ### Events
392
454
 
393
- list.on('load:end', ({ items, total }) => {
394
- console.log('Loaded', items.length, 'of', total);
395
- });
455
+ ```typescript
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 }) => { })
396
465
  ```
397
466
 
398
- ### Scroll Save/Restore
467
+ ## Configuration
468
+
469
+ ### Base Configuration
399
470
 
400
471
  ```typescript
401
- const list = createVList({
402
- container: '#my-list',
472
+ interface VListConfig<T> {
473
+ // Required
474
+ container: HTMLElement | string;
403
475
  item: {
404
- height: 64,
405
- template: (item) => `<div>${item.name}</div>`,
406
- },
407
- items: myItems,
408
- selection: { mode: 'multiple' },
409
- });
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
+ };
410
480
 
411
- // Save — e.g. before navigating away
412
- const snapshot = list.getScrollSnapshot();
413
- // { index: 523, offsetInItem: 12, selectedIds: [3, 7, 42] }
414
- sessionStorage.setItem('list-scroll', JSON.stringify(snapshot));
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
415
488
 
416
- // Restore — e.g. after navigating back and recreating the list
417
- const saved = JSON.parse(sessionStorage.getItem('list-scroll'));
418
- list.restoreScroll(saved);
419
- // Scroll position AND selection are perfectly restored
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
+ }
420
496
  ```
421
497
 
422
- ### Framework Adapters
498
+ ### Plugin: `withGrid(config)`
423
499
 
424
- 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.
500
+ 2D grid layout with virtualized rows.
425
501
 
426
- #### React
502
+ ```typescript
503
+ interface GridConfig {
504
+ columns: number; // Number of columns (required)
505
+ gap?: number; // Gap between items in pixels (default: 0)
506
+ }
507
+ ```
427
508
 
428
- ```tsx
429
- import { useVList, useVListEvent } from 'vlist/react';
509
+ **Example:**
510
+ ```typescript
511
+ .use(withGrid({ columns: 4, gap: 16 }))
512
+ ```
430
513
 
431
- function UserList({ users }) {
432
- const { containerRef, instanceRef } = useVList({
433
- item: {
434
- height: 48,
435
- template: (user) => `<div class="user">${user.name}</div>`,
436
- },
437
- items: users,
438
- selection: { mode: 'single' },
439
- });
514
+ ### Plugin: `withSections(config)`
440
515
 
441
- // Optional: subscribe to events with automatic cleanup
442
- useVListEvent(instanceRef, 'selection:change', ({ selected }) => {
443
- console.log('Selected:', selected);
444
- });
516
+ Grouped lists with sticky or inline headers.
445
517
 
446
- return (
447
- <div
448
- ref={containerRef}
449
- style={{ height: 400 }}
450
- onClick={() => instanceRef.current?.scrollToIndex(0)}
451
- />
452
- );
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)
453
524
  }
454
525
  ```
455
526
 
456
- `useVList` returns:
457
- - `containerRef` — attach to your container `<div>`
458
- - `instanceRef` — ref to the `VList` instance (populated after mount)
459
- - `getInstance()` stable helper to access the instance
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
+ ```
460
536
 
461
- Items auto-sync when `config.items` changes by reference.
537
+ **Important:** Items must be pre-sorted by group!
462
538
 
463
- #### Vue
539
+ ### Plugin: `withSelection(config)`
464
540
 
465
- ```vue
466
- <template>
467
- <div ref="containerRef" style="height: 400px" />
468
- </template>
541
+ Single or multiple item selection with keyboard navigation.
469
542
 
470
- <script setup lang="ts">
471
- import { useVList, useVListEvent } from 'vlist/vue';
472
- import { ref } from 'vue';
543
+ ```typescript
544
+ interface SelectionConfig {
545
+ mode: 'single' | 'multiple';
546
+ initial?: Array<string | number>; // Pre-selected item IDs
547
+ }
548
+ ```
473
549
 
474
- const users = ref([
475
- { id: 1, name: 'Alice' },
476
- { id: 2, name: 'Bob' },
477
- ]);
550
+ **Example:**
551
+ ```typescript
552
+ .use(withSelection({
553
+ mode: 'multiple',
554
+ initial: [1, 5, 10],
555
+ }))
556
+ ```
478
557
 
479
- const { containerRef, instance } = useVList({
480
- item: {
481
- height: 48,
482
- template: (user) => `<div class="user">${user.name}</div>`,
483
- },
484
- items: users.value,
485
- });
558
+ ### Plugin: `withAsync(config)`
486
559
 
487
- // Optional: subscribe to events with automatic cleanup
488
- useVListEvent(instance, 'selection:change', ({ selected }) => {
489
- console.log('Selected:', selected);
490
- });
560
+ Asynchronous data loading with lazy loading and placeholders.
491
561
 
492
- function jumpToTop() {
493
- instance.value?.scrollToIndex(0);
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
+ };
494
574
  }
495
- </script>
496
575
  ```
497
576
 
498
- `useVList` accepts a plain config or a reactive `Ref<Config>`. When using a ref, items are watched and synced automatically.
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
+ ```
499
591
 
500
- #### Svelte
592
+ ### Plugin: `withScale()`
501
593
 
502
- ```svelte
503
- <script>
504
- import { vlist, onVListEvent } from 'vlist/svelte';
594
+ Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
505
595
 
506
- let instance;
507
- let unsubs = [];
596
+ ```typescript
597
+ .use(withScale()) // No config needed - auto-activates
598
+ ```
508
599
 
509
- const options = {
510
- config: {
511
- item: {
512
- height: 48,
513
- template: (user) => `<div class="user">${user.name}</div>`,
514
- },
515
- items: users,
516
- selection: { mode: 'single' },
517
- },
518
- onInstance: (inst) => {
519
- instance = inst;
520
- unsubs.push(
521
- onVListEvent(inst, 'selection:change', ({ selected }) => {
522
- console.log('Selected:', selected);
523
- })
524
- );
525
- },
526
- };
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
+ ```
527
610
 
528
- import { onDestroy } from 'svelte';
529
- onDestroy(() => unsubs.forEach(fn => fn()));
530
- </script>
611
+ ### Plugin: `withScrollbar(config)`
612
+
613
+ Custom scrollbar with auto-hide and smooth dragging.
531
614
 
532
- <div use:vlist={options} style="height: 400px" />
533
- <button on:click={() => instance?.scrollToIndex(0)}>Jump to top</button>
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
+ }))
534
629
  ```
535
630
 
536
- 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.
631
+ ### Plugin: `withPage()`
537
632
 
538
- ### With Custom Template
633
+ Use document-level scrolling instead of container scrolling.
539
634
 
540
635
  ```typescript
541
- const list = createVList({
542
- container: '#my-list',
543
- item: {
544
- height: 72,
545
- template: (item, index, { selected, focused }) => {
546
- // Return an HTMLElement for more control
547
- const el = document.createElement('div');
548
- el.className = 'item-content';
549
- el.innerHTML = `
550
- <img src="${item.avatar}" class="avatar avatar--large" />
551
- <div class="item-details">
552
- <div class="item-name">${item.name}</div>
553
- <div class="item-email">${item.email}</div>
554
- </div>
555
- <div class="item-role">${item.role}</div>
556
- `;
557
- return el;
558
- },
559
- },
560
- items: users,
561
- });
636
+ .use(withPage()) // No config needed
562
637
  ```
563
638
 
564
- ## API Reference
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
+ ```
565
649
 
566
- ### Methods
650
+ Perfect for blog posts, infinite scroll feeds, and full-page lists.
567
651
 
568
- #### Data Management
652
+ ### Plugin: `withSnapshots()`
653
+
654
+ Scroll position save/restore for SPA navigation.
569
655
 
570
656
  ```typescript
571
- list.setItems(items: T[]) // Replace all items
572
- list.appendItems(items: T[]) // Add items to end
573
- list.prependItems(items: T[]) // Add items to start
574
- list.updateItem(id, updates) // Update item by ID
575
- list.removeItem(id) // Remove item by ID
576
- list.reload() // Reload from adapter
657
+ // Included in base - no need to import
658
+ const snapshot = list.getScrollSnapshot();
659
+ list.restoreScroll(snapshot);
577
660
  ```
578
661
 
579
- #### Scrolling
580
-
662
+ **Example:**
581
663
  ```typescript
582
- list.scrollToIndex(index, align?) // Scroll to index ('start' | 'center' | 'end')
583
- list.scrollToIndex(index, options?) // Scroll with options (smooth scrolling)
584
- list.scrollToItem(id, align?) // Scroll to item by ID
585
- list.scrollToItem(id, options?) // Scroll to item with options
586
- list.cancelScroll() // Cancel in-progress smooth scroll
587
- list.getScrollPosition() // Get current scroll position
588
- list.getScrollSnapshot() // Get snapshot for save/restore
589
- list.restoreScroll(snapshot) // Restore position (and selection) from snapshot
664
+ // Save on navigation away
665
+ const snapshot = list.getScrollSnapshot();
666
+ sessionStorage.setItem('scroll', JSON.stringify(snapshot));
590
667
 
591
- // ScrollToOptions: { align?, behavior?: 'auto' | 'smooth', duration? }
592
- // Example: list.scrollToIndex(500, { align: 'center', behavior: 'smooth' })
593
- // ScrollSnapshot: { index, offsetInItem, selectedIds? } — JSON-serializable
668
+ // Restore on return
669
+ const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
670
+ list.restoreScroll(snapshot);
594
671
  ```
595
672
 
596
- #### Selection
673
+ ## Advanced Usage
674
+
675
+ ### Variable Heights with DOM Measurement
597
676
 
598
677
  ```typescript
599
- list.select(...ids) // Select items
600
- list.deselect(...ids) // Deselect items
601
- list.toggleSelect(id) // Toggle selection
602
- list.selectAll() // Select all
603
- list.clearSelection() // Clear selection
604
- list.getSelected() // Get selected IDs
605
- list.getSelectedItems() // Get selected items
678
+ import { vlist } from 'vlist';
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();
606
701
  ```
607
702
 
608
- #### Events
703
+ ### Dynamic Grid Columns
609
704
 
610
705
  ```typescript
611
- list.on(event, handler) // Subscribe to event
612
- list.off(event, handler) // Unsubscribe from event
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
+ });
613
730
  ```
614
731
 
615
- #### Lifecycle
732
+ ### Combining Multiple Plugins
616
733
 
617
734
  ```typescript
618
- list.destroy() // Cleanup and remove
735
+ import {
736
+ vlist,
737
+ withGrid,
738
+ withSections,
739
+ withSelection,
740
+ withAsync,
741
+ withScrollbar
742
+ } from 'vlist';
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();
619
761
  ```
620
762
 
621
- ### Properties
763
+ **Bundle:** ~15 KB gzipped (includes only used plugins)
764
+
765
+ ## Framework Adapters
766
+
767
+ ### React
622
768
 
623
769
  ```typescript
624
- list.element // Root DOM element
625
- list.items // Current items (readonly)
626
- list.total // Total item count
627
- ```
770
+ import { vlist, withSelection } from 'vlist';
771
+ import { useEffect, useRef } from 'react';
628
772
 
629
- ### Events
773
+ function MyList({ items }) {
774
+ const containerRef = useRef(null);
775
+ const listRef = useRef(null);
630
776
 
631
- | Event | Payload | Description |
632
- |-------|---------|-------------|
633
- | `item:click` | `{ item, index, event }` | Item was clicked |
634
- | `selection:change` | `{ selected, items }` | Selection changed |
635
- | `scroll` | `{ scrollTop, direction }` | Scroll position changed |
636
- | `range:change` | `{ range }` | Visible range changed |
637
- | `resize` | `{ height, width }` | Container was resized |
638
- | `load:start` | `{ offset, limit }` | Data loading started |
639
- | `load:end` | `{ items, total }` | Data loading completed |
640
- | `error` | `{ error, context }` | Error occurred |
777
+ useEffect(() => {
778
+ if (!containerRef.current) return;
641
779
 
642
- ## Keyboard Navigation & Accessibility
780
+ listRef.current = vlist({
781
+ container: containerRef.current,
782
+ items,
783
+ item: { height: 48, template: renderItem },
784
+ })
785
+ .use(withSelection({ mode: 'single' }))
786
+ .build();
643
787
 
644
- vlist implements the [WAI-ARIA Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) for full screen reader and keyboard support.
788
+ return () => listRef.current?.destroy();
789
+ }, []);
645
790
 
646
- ### Keyboard Shortcuts
791
+ useEffect(() => {
792
+ listRef.current?.setItems(items);
793
+ }, [items]);
794
+
795
+ return <div ref={containerRef} />;
796
+ }
797
+ ```
647
798
 
648
- When selection is enabled, the list supports full keyboard navigation:
799
+ ### Vue 3
649
800
 
650
- | Key | Action |
651
- |-----|--------|
652
- | `↑` / `↓` | Move focus up/down |
653
- | `Home` | Move focus to first item |
654
- | `End` | Move focus to last item |
655
- | `Space` / `Enter` | Toggle selection on focused item |
656
- | `Tab` | Move focus into / out of the list |
801
+ ```typescript
802
+ import { vlist, withSelection } from 'vlist';
803
+ import { ref, onMounted, onUnmounted, watch } from 'vue';
804
+
805
+ export default {
806
+ setup() {
807
+ const container = ref(null);
808
+ const list = ref(null);
809
+
810
+ onMounted(() => {
811
+ list.value = vlist({
812
+ container: container.value,
813
+ items: items.value,
814
+ item: { height: 48, template: renderItem },
815
+ })
816
+ .use(withSelection({ mode: 'single' }))
817
+ .build();
818
+ });
819
+
820
+ watch(items, (newItems) => {
821
+ list.value?.setItems(newItems);
822
+ });
823
+
824
+ onUnmounted(() => {
825
+ list.value?.destroy();
826
+ });
827
+
828
+ return { container };
829
+ },
830
+ };
831
+ ```
657
832
 
658
- ### ARIA Attributes
833
+ ### Svelte
659
834
 
660
- | Attribute | Element | Purpose |
661
- |-----------|---------|---------|
662
- | `role="listbox"` | Root | Identifies the widget as a list of selectable items |
663
- | `role="option"` | Each item | Identifies each item as a selectable option |
664
- | `aria-setsize` | Each item | Total item count — screen readers announce "item 5 of 10,000" |
665
- | `aria-posinset` | Each item | 1-based position within the list |
666
- | `aria-activedescendant` | Root | Points to the focused item's ID for screen reader tracking |
667
- | `aria-selected` | Each item | Reflects selection state |
668
- | `aria-busy` | Root | Present during async data loading |
669
- | `aria-label` | Root | Set via `ariaLabel` config option |
835
+ ```typescript
836
+ <script>
837
+ import { vlist, withSelection } from 'vlist';
838
+ import { onMount, onDestroy } from 'svelte';
839
+
840
+ export let items = [];
841
+
842
+ let container;
843
+ let list;
844
+
845
+ onMount(() => {
846
+ list = vlist({
847
+ container,
848
+ items,
849
+ item: { height: 48, template: renderItem },
850
+ })
851
+ .use(withSelection({ mode: 'single' }))
852
+ .build();
853
+ });
670
854
 
671
- A visually-hidden **live region** (`aria-live="polite"`) announces selection changes (e.g., "3 items selected").
855
+ $: list?.setItems(items);
672
856
 
673
- Each item receives a unique `id` (`vlist-{instance}-item-{index}`) safe for multiple lists per page.
857
+ onDestroy(() => {
858
+ list?.destroy();
859
+ });
860
+ </script>
674
861
 
675
- > 📖 Full documentation: [docs/accessibility.md](docs/accessibility.md)
862
+ <div bind:this={container}></div>
863
+ ```
676
864
 
677
865
  ## Styling
678
866
 
679
- ### Default Styles
680
-
681
- Import the default styles:
867
+ Import the base styles:
682
868
 
683
869
  ```typescript
684
870
  import 'vlist/styles';
685
871
  ```
686
872
 
687
- Optional extras (variants, loading states, animations):
873
+ Or customize with your own CSS:
688
874
 
689
- ```typescript
690
- import 'vlist/styles/extras';
691
- ```
875
+ ```css
876
+ .vlist {
877
+ /* Container styles */
878
+ }
692
879
 
693
- ### CSS Classes
880
+ .vlist-viewport {
881
+ /* Scroll viewport */
882
+ }
694
883
 
695
- The component uses these CSS class names:
884
+ .vlist-item {
885
+ /* Item wrapper - positioned absolutely */
886
+ }
696
887
 
697
- - `.vlist` - Root container
698
- - `.vlist-viewport` - Scrollable viewport
699
- - `.vlist-content` - Content container (sets total height)
700
- - `.vlist-items` - Items container
701
- - `.vlist-item` - Individual item
702
- - `.vlist-item--selected` - Selected item
703
- - `.vlist-item--focused` - Focused item (keyboard nav)
704
- - `.vlist--grid` - Grid layout modifier
705
- - `.vlist-grid-item` - Grid item (positioned with `translate(x, y)`)
706
- - `.vlist--grouped` - Grouped list modifier
707
- - `.vlist-sticky-header` - Sticky header overlay
708
- - `.vlist-live-region` - Visually-hidden live region for screen reader announcements
709
- - `.vlist--scrolling` - Applied during active scroll (disables transitions)
888
+ .vlist-item--selected {
889
+ /* Selected state */
890
+ }
710
891
 
711
- ### Variants
892
+ .vlist-item--focused {
893
+ /* Focused state (keyboard navigation) */
894
+ }
712
895
 
713
- Import `vlist/styles/extras` for these variant classes:
896
+ .vlist-scrollbar {
897
+ /* Custom scrollbar track */
898
+ }
714
899
 
715
- ```html
716
- <!-- Compact spacing -->
717
- <div class="vlist vlist--compact">...</div>
900
+ .vlist-scrollbar__thumb {
901
+ /* Scrollbar thumb/handle */
902
+ }
903
+ ```
718
904
 
719
- <!-- Comfortable spacing -->
720
- <div class="vlist vlist--comfortable">...</div>
905
+ ## Performance
721
906
 
722
- <!-- No borders -->
723
- <div class="vlist vlist--borderless">...</div>
907
+ ### Bundle Sizes (Gzipped)
724
908
 
725
- <!-- Striped rows -->
726
- <div class="vlist vlist--striped">...</div>
727
- ```
909
+ | Configuration | Size | What's Included |
910
+ |---------------|------|-----------------|
911
+ | Base only | 7.7 KB | Core virtualization |
912
+ | + Grid | 11.7 KB | + 2D layout |
913
+ | + Sections | 12.3 KB | + Grouped lists |
914
+ | + Selection | 10.0 KB | + Item selection |
915
+ | + Async | 13.5 KB | + Data loading |
916
+ | + Scale | 9.9 KB | + 1M+ items |
917
+ | + All plugins | ~16 KB | Everything |
728
918
 
729
- ### CSS Custom Properties
919
+ **Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
730
920
 
731
- All visual aspects can be customized via CSS custom properties:
921
+ ### Memory Efficiency
732
922
 
733
- ```css
734
- :root {
735
- --vlist-bg: #ffffff;
736
- --vlist-bg-hover: #f9fafb;
737
- --vlist-bg-selected: #eff6ff;
738
- --vlist-border: #e5e7eb;
739
- --vlist-text: #111827;
740
- --vlist-focus-ring: #3b82f6;
741
- --vlist-item-padding-x: 1rem;
742
- --vlist-item-padding-y: 0.75rem;
743
- --vlist-border-radius: 0.5rem;
744
- --vlist-transition-duration: 150ms;
745
- }
746
- ```
923
+ With 100,000 items at 48px each:
924
+ - **Total height:** 4,800,000 pixels
925
+ - **Visible items:** ~20 (depending on viewport height)
926
+ - **DOM nodes:** ~26 (visible + overscan)
927
+ - **Memory saved:** ~99.97% (26 DOM nodes vs 100,000)
928
+
929
+ ### Benchmarks
930
+
931
+ See [benchmarks](https://vlist.dev/benchmarks/) for detailed performance comparisons.
932
+
933
+ ## Browser Support
934
+
935
+ - Chrome/Edge: Latest 2 versions
936
+ - Firefox: Latest 2 versions
937
+ - Safari: Latest 2 versions
938
+ - iOS Safari: 12.4+
939
+ - Chrome Android: Latest
747
940
 
748
- Dark mode is supported automatically via `prefers-color-scheme: dark` or the `.dark` class.
941
+ Relies on:
942
+ - `IntersectionObserver` (widely supported)
943
+ - `ResizeObserver` (polyfill available if needed)
944
+ - CSS `transform` (universal support)
749
945
 
750
946
  ## TypeScript
751
947
 
752
- Full TypeScript support with generics:
948
+ Fully typed with comprehensive TypeScript definitions:
753
949
 
754
950
  ```typescript
755
- interface User {
951
+ import { vlist, withGrid, type VList, type VListConfig } from 'vlist';
952
+
953
+ interface Photo {
756
954
  id: number;
757
- name: string;
758
- email: string;
955
+ url: string;
956
+ title: string;
759
957
  }
760
958
 
761
- const list = createVList<User>({
762
- container: '#users',
959
+ const config: VListConfig<Photo> = {
960
+ container: '#gallery',
961
+ items: photos,
763
962
  item: {
764
- height: 48,
765
- template: (user) => `<div>${user.name} - ${user.email}</div>`,
963
+ height: 200,
964
+ template: (photo: Photo) => `<img src="${photo.url}" />`,
766
965
  },
767
- items: users,
768
- });
966
+ };
769
967
 
770
- // Fully typed
771
- list.on('item:click', ({ item }) => {
772
- console.log(item.email); // TypeScript knows this is a User
773
- });
968
+ const list: VList<Photo> = vlist(config)
969
+ .use(withGrid({ columns: 4 }))
970
+ .build();
774
971
  ```
775
972
 
776
- ## Performance
777
-
778
- vlist is designed for maximum performance with extensive built-in optimizations:
973
+ ## Migration from Monolithic API
779
974
 
780
- - **Virtual rendering** - Only visible items + overscan buffer are in the DOM
781
- - **Element pooling** - DOM elements are recycled via `createElementPool()`, reducing GC pressure
782
- - **Zero-allocation scroll hot path** - No object allocations per scroll frame; in-place range mutation
783
- - **RAF-throttled native scroll** - At most one scroll processing per animation frame
784
- - **CSS containment** - `contain: layout style` on items container, `contain: content` + `will-change: transform` on items
785
- - **Scroll transition suppression** - `.vlist--scrolling` class disables CSS transitions during active scroll
786
- - **Circular buffer velocity tracker** - Pre-allocated buffer, zero allocations during scroll
787
- - **Targeted keyboard focus render** - Arrow keys update only 2 affected items instead of all visible items
788
- - **Batched LRU timestamps** - Single `Date.now()` per render cycle instead of per-item
789
- - **DocumentFragment batching** - New elements appended in a single DOM operation
790
- - **Split CSS** - Core styles (5.6 KB) separated from optional extras (1.8 KB)
791
- - **Configurable velocity-based loading** - Skip, preload, or defer loading based on scroll speed
792
- - **Compression for 1M+ items** - Automatic scroll space compression when content exceeds browser height limits
975
+ If you're using the old monolithic API:
793
976
 
794
- ### Benchmark Results
977
+ ### Before (Monolithic - Deprecated)
795
978
 
796
- Measured in Chrome (10-core Mac, 60Hz display) via the [live benchmark page](https://vlist.dev/benchmarks/):
979
+ ```typescript
980
+ import { createVList } from 'vlist';
797
981
 
798
- | Metric | 10K items | 1M items |
799
- |--------|-----------|----------|
800
- | Initial render | ~32ms | ~135ms |
801
- | Scroll FPS | 60fps | 61fps |
802
- | Frame budget (avg) | 2.1ms | 1.9ms |
803
- | Frame budget (p95) | 4.2ms | 8.7ms |
804
- | Dropped frames | 0% | 0% |
805
- | scrollToIndex | ~166ms | ~82ms |
806
- | Memory (scroll delta) | 0 MB | 0 MB |
982
+ const list = createVList({
983
+ container: '#app',
984
+ items: data,
985
+ grid: { columns: 4 },
986
+ groups: { ... },
987
+ selection: { mode: 'single' },
988
+ });
989
+ ```
807
990
 
808
- Zero dropped frames and zero memory growth during sustained scrolling — even at 1M items.
991
+ ### After (Builder - Recommended)
809
992
 
810
- For the full optimization guide, see [docs/optimization.md](docs/optimization.md).
993
+ ```typescript
994
+ import { vlist, withGrid, withSections, withSelection } from 'vlist';
995
+
996
+ const list = vlist({
997
+ container: '#app',
998
+ items: data,
999
+ })
1000
+ .use(withGrid({ columns: 4 }))
1001
+ .use(withSections({ ... }))
1002
+ .use(withSelection({ mode: 'single' }))
1003
+ .build();
1004
+ ```
811
1005
 
812
- ## Browser Support
1006
+ **Benefits:**
1007
+ - 2-3x smaller bundles (20 KB → 8-12 KB gzipped)
1008
+ - Explicit about what's included
1009
+ - Better tree-shaking
1010
+ - Easier to understand and debug
813
1011
 
814
- - Chrome 60+
815
- - Firefox 55+
816
- - Safari 12+
817
- - Edge 79+
1012
+ ## Plugin Naming Changes
818
1013
 
819
- ## Contributing
1014
+ If you're upgrading from an earlier version:
820
1015
 
821
- Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
1016
+ | Old Name | New Name | Import |
1017
+ |----------|----------|--------|
1018
+ | `withCompression()` | `withScale()` | `import { withScale } from 'vlist'` |
1019
+ | `withData()` | `withAsync()` | `import { withAsync } from 'vlist'` |
1020
+ | `withWindow()` | `withPage()` | `import { withPage } from 'vlist'` |
1021
+ | `withGroups()` | `withSections()` | `import { withSections } from 'vlist'` |
1022
+ | `withScroll()` | `withScrollbar()` | `import { withScrollbar } from 'vlist'` |
822
1023
 
823
- ```bash
824
- # Install dependencies
825
- bun install
1024
+ ## Contributing
826
1025
 
827
- # Run development build
828
- bun run dev
1026
+ Contributions are welcome! Please:
829
1027
 
830
- # Run tests
831
- bun test
1028
+ 1. Fork the repository
1029
+ 2. Create a feature branch
1030
+ 3. Make your changes
1031
+ 4. Add/update tests
1032
+ 5. Submit a pull request
832
1033
 
833
- # Type check
834
- bun run typecheck
1034
+ ## License
835
1035
 
836
- # Build for production
837
- bun run build
838
- ```
1036
+ MIT License - see [LICENSE](LICENSE) for details
839
1037
 
840
- ## License
1038
+ ## Links
841
1039
 
842
- MIT © [Floor](https://github.com/floor)
1040
+ - **Documentation & Examples:** [vlist.dev](https://vlist.dev)
1041
+ - **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
1042
+ - **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
1043
+ - **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
843
1044
 
844
- ## Credits
1045
+ ---
845
1046
 
846
- Inspired by the [mtrl-addons](https://github.com/floor/mtrl-addons) vlist component.
1047
+ **Built by [Floor IO](https://floor.io)** with ❤️ for the web community