virtua 0.52.10 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +286 -193
  2. package/lib/angular/VList.d.ts +3 -1
  3. package/lib/angular/VMasonry.d.ts +119 -0
  4. package/lib/angular/Virtualizer.d.ts +3 -1
  5. package/lib/angular/WindowVirtualizer.d.ts +3 -1
  6. package/lib/angular/index.d.ts +1 -0
  7. package/lib/angular/index.js +732 -333
  8. package/lib/angular/index.js.map +1 -1
  9. package/lib/core/index.cjs +180 -104
  10. package/lib/core/index.cjs.map +1 -1
  11. package/lib/core/index.d.ts +1 -0
  12. package/lib/core/index.js +257 -181
  13. package/lib/core/index.js.map +1 -1
  14. package/lib/core/layouts/masonry.d.ts +1 -0
  15. package/lib/index.cjs +562 -385
  16. package/lib/index.cjs.map +1 -1
  17. package/lib/index.js +579 -404
  18. package/lib/index.js.map +1 -1
  19. package/lib/react/VMasonry.d.ts +109 -0
  20. package/lib/react/Virtualizer.d.ts +3 -1
  21. package/lib/react/WindowVirtualizer.d.ts +3 -1
  22. package/lib/react/index.d.ts +2 -0
  23. package/lib/react/types.d.ts +1 -1
  24. package/lib/solid/VMasonry.d.ts +111 -0
  25. package/lib/solid/Virtualizer.d.ts +3 -1
  26. package/lib/solid/WindowVirtualizer.d.ts +3 -1
  27. package/lib/solid/index.cjs +726 -514
  28. package/lib/solid/index.cjs.map +1 -1
  29. package/lib/solid/index.d.ts +2 -0
  30. package/lib/solid/index.js +697 -494
  31. package/lib/solid/index.js.map +1 -1
  32. package/lib/solid/index.jsx +343 -8
  33. package/lib/solid/index.jsx.map +1 -1
  34. package/lib/solid/types.d.ts +1 -1
  35. package/lib/svelte/MasonryItem.svelte +49 -0
  36. package/lib/svelte/VGrid.svelte +1 -1
  37. package/lib/svelte/VList.svelte +1 -1
  38. package/lib/svelte/VMasonry.svelte +196 -0
  39. package/lib/svelte/VMasonry.type.d.ts +108 -0
  40. package/lib/svelte/Virtualizer.svelte +1 -1
  41. package/lib/svelte/Virtualizer.type.d.ts +3 -1
  42. package/lib/svelte/WindowVirtualizer.svelte +1 -1
  43. package/lib/svelte/WindowVirtualizer.type.d.ts +3 -1
  44. package/lib/svelte/index.d.ts +2 -0
  45. package/lib/svelte/index.js +2 -1
  46. package/lib/svelte/types.d.ts +2 -2
  47. package/lib/vue/VMasonry.d.ts +114 -0
  48. package/lib/vue/Virtualizer.d.ts +3 -1
  49. package/lib/vue/WindowVirtualizer.d.ts +3 -1
  50. package/lib/vue/index.cjs +632 -403
  51. package/lib/vue/index.cjs.map +1 -1
  52. package/lib/vue/index.d.ts +2 -0
  53. package/lib/vue/index.js +679 -455
  54. package/lib/vue/index.js.map +1 -1
  55. package/package.json +6 -16
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/virtua) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/virtua) ![npm](https://img.shields.io/npm/dw/virtua) [![Best of JS](https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=inokawa%2Fvirtua%26since=daily)](https://bestofjs.org/projects/virtua) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/inokawa/virtua) [![check](https://github.com/inokawa/virtua/actions/workflows/check.yml/badge.svg)](https://github.com/inokawa/virtua/actions/workflows/check.yml) [![demo](https://github.com/inokawa/virtua/actions/workflows/demo.yml/badge.svg)](https://github.com/inokawa/virtua/actions/workflows/demo.yml)
4
4
 
5
- > A zero-config, fast and small virtual list and grid component for [React](https://github.com/facebook/react), [Vue](https://vuejs.org/), [Solid](https://www.solidjs.com/), [Svelte](https://svelte.dev/) and [Angular](https://angular.dev/).
5
+ > A zero-config, fast and small virtual list, grid and masonry component for [React](https://github.com/facebook/react), [Vue](https://vuejs.org/), [Solid](https://www.solidjs.com/), [Svelte](https://svelte.dev/) and [Angular](https://angular.dev/).
6
6
 
7
7
  ![list example](./example.gif) ![grid example](./example2.gif)
8
8
 
@@ -15,7 +15,7 @@ This project is a challenge to rethink virtualization. The goals are...
15
15
  - **Zero-config virtualization:** This library is designed to give the best performance without configuration. It also handles common hard things in the real world (dynamic size measurement, scroll position adjustment while reverse scrolling and imperative scrolling, iOS support, etc).
16
16
  - **Fast:** Natural virtual scrolling needs optimization in many aspects (eliminate frame drops by reducing CPU usage and GC, reduce [synchronous layout recalculation](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), reduce visual jumps on repaint, optimize with CSS, optimize for JIT, optimize for frameworks, etc). We are trying to combine the best of them.
17
17
  - **Small:** Its bundle size should be small as much as possible to be friendly with modern web development. Currently components start from ~3kB gzipped and are tree-shakeable.
18
- - **Flexible:** Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, table, RTL, mobile, infinite scrolling, scroll restoration, DnD, keyboard navigation, sticky and more. See [live demo](#demo).
18
+ - **Flexible:** Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, table, masonry, RTL, mobile, infinite scrolling, scroll restoration, DnD, keyboard navigation, sticky and more. See [live demo](#demo).
19
19
  - **Framework agnostic:** [React](https://react.dev/), [Vue](https://vuejs.org/), [Solid](https://www.solidjs.com/), [Svelte](https://svelte.dev/) and [Angular](https://angular.dev/) are supported. We could support other frameworks in the future.
20
20
 
21
21
  ## Demo
@@ -33,7 +33,7 @@ If you want to support legacy browsers, you need polyfills.
33
33
  - [ResizeObserver](https://caniuse.com/?search=resizeobserver) is always required (e.g. [@juggle/resize-observer](https://github.com/juggle/resize-observer#switching-between-native-and-polyfilled-versions)).
34
34
  - [Scroll methods on elements](https://caniuse.com/element-scroll-methods) are always required (e.g. [element-scroll-polyfill](https://github.com/idmadj/element-scroll-polyfill)).
35
35
  - [CSS scroll-behavior](https://caniuse.com/?search=scroll-behavior) is required only if you call `scrollToIndex` with `smooth: true` (e.g. [scroll-behavior-polyfill](https://github.com/wessberg/scroll-behavior-polyfill)).
36
- - [CSS inset-inline-start](https://caniuse.com/mdn-css_properties_inset-inline-start) is required only if you set `horizontal: true`. It cannot be polyfilled so use `virtua@<=0.50` instead.
36
+ - [CSS inset-inline-start](https://caniuse.com/mdn-css_properties_inset-inline-start) is required only if you set `horizontal: true` or use `VMasonry`. It cannot be polyfilled so use `virtua@<=0.50` instead.
37
37
  - [CSS subgrid](https://caniuse.com/css-subgrid) is required for VGrid.
38
38
 
39
39
  ## Getting started
@@ -44,48 +44,93 @@ If you want to support legacy browsers, you need polyfills.
44
44
 
45
45
  If you use ESM and webpack 5, use react >= 18 to avoid [Can't resolve `react/jsx-runtime` error](https://github.com/facebook/react/issues/20235).
46
46
 
47
- #### Vertical scroll
48
-
49
47
  ```tsx
50
48
  import { VList } from "virtua";
51
49
 
52
- // children
50
+ const sizes = [20, 40, 80, 77];
51
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
52
+ id: i,
53
+ size: sizes[i % 4],
54
+ }));
55
+
53
56
  export const App = () => {
54
57
  return (
55
- <VList style={{ height: 800 }}>
56
- {Array.from({ length: 1000 }).map((_, i) => (
58
+ <VList data={data} style={{ height: 800 }}>
59
+ {(item) => (
57
60
  <div
58
- key={i}
61
+ key={item.id}
59
62
  style={{
60
- height: Math.floor(Math.random() * 10) * 10 + 10,
61
- borderBottom: "solid 1px gray",
63
+ height: item.size,
62
64
  background: "white",
65
+ borderBottom: "solid 1px #ccc",
63
66
  }}
64
67
  >
65
- {i}
68
+ {item.id}
66
69
  </div>
67
- ))}
70
+ )}
68
71
  </VList>
69
72
  );
70
73
  };
74
+ ```
75
+
76
+ You can also pass elements as `children` directly instead of `data`.
77
+
78
+ ### Vue
79
+
80
+ `vue >= 3.2` is required.
81
+
82
+ ```vue
83
+ <script setup>
84
+ import { VList } from "virtua/vue";
85
+
86
+ const sizes = [20, 40, 80, 77];
87
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
88
+ id: i,
89
+ size: sizes[i % 4],
90
+ }));
91
+ </script>
92
+
93
+ <template>
94
+ <VList :data="data" :style="{ height: '800px' }" #default="{ item }">
95
+ <div
96
+ :key="item.id"
97
+ :style="{
98
+ height: item.size + 'px',
99
+ background: 'white',
100
+ borderBottom: 'solid 1px #ccc',
101
+ }"
102
+ >
103
+ {{ item.id }}
104
+ </div>
105
+ </VList>
106
+ </template>
107
+ ```
108
+
109
+ ### Solid
110
+
111
+ `solid-js >= 1.0` is required.
112
+
113
+ ```tsx
114
+ import { VList } from "virtua/solid";
115
+
116
+ const sizes = [20, 40, 80, 77];
117
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
118
+ id: i,
119
+ size: sizes[i % 4],
120
+ }));
71
121
 
72
- // or render prop
73
122
  export const App = () => {
74
- const items = Array.from({ length: 1000 }).map(
75
- () => Math.floor(Math.random() * 10) * 10 + 10,
76
- );
77
123
  return (
78
- <VList data={items} style={{ height: 800 }}>
79
- {(d, i) => (
124
+ <VList data={data} style={{ height: "800px" }}>
125
+ {(item) => (
80
126
  <div
81
- key={i}
82
127
  style={{
83
- height: d,
84
- borderBottom: "solid 1px gray",
128
+ height: item.size + "px",
85
129
  background: "white",
130
+ "border-bottom": "solid 1px #ccc",
86
131
  }}
87
132
  >
88
- {i}
133
+ {item.id}
89
134
  </div>
90
135
  )}
91
136
  </VList>
@@ -93,89 +138,191 @@ export const App = () => {
93
138
  };
94
139
  ```
95
140
 
96
- #### Horizontal scroll
141
+ ### Svelte
142
+
143
+ `svelte >= 5.0` is required.
144
+
145
+ ```svelte
146
+ <script lang="ts">
147
+ import { VList } from "virtua/svelte";
148
+
149
+ const sizes = [20, 40, 80, 77];
150
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
151
+ id: i,
152
+ size: sizes[i % 4],
153
+ }));
154
+ </script>
155
+
156
+ <VList {data} style="height: 800px;" getKey={(item) => item.id}>
157
+ {#snippet children(item)}
158
+ <div
159
+ style="
160
+ height: {item.size}px;
161
+ background: white;
162
+ border-bottom: solid 1px #ccc;
163
+ "
164
+ >
165
+ {item.id}
166
+ </div>
167
+ {/snippet}
168
+ </VList>
169
+ ```
170
+
171
+ ### Angular
172
+
173
+ `@angular/core >= 20` is required. The components are zoneless-ready and work with both zone.js and [`provideZonelessChangeDetection`](https://angular.dev/api/core/provideZonelessChangeDetection).
174
+
175
+ ```ts
176
+ import { Component } from "@angular/core";
177
+ import { VList } from "virtua/angular";
178
+
179
+ const sizes = [20, 40, 80, 77];
180
+
181
+ @Component({
182
+ selector: "app-root",
183
+ imports: [VList],
184
+ template: `
185
+ <virtua-vlist [data]="data" [getKey]="getKey" style="height: 800px;">
186
+ <ng-template let-item>
187
+ <div
188
+ [style.height.px]="item.size"
189
+ style="background: white; border-bottom: solid 1px #ccc;"
190
+ >
191
+ {{ item.id }}
192
+ </div>
193
+ </ng-template>
194
+ </virtua-vlist>
195
+ `,
196
+ })
197
+ export class App {
198
+ protected readonly data = Array.from({ length: 1000 }).map((_, i) => ({
199
+ id: i,
200
+ size: sizes[i % 4],
201
+ }));
202
+ protected readonly getKey = (item: { id: number }) => item.id;
203
+ }
204
+ ```
205
+
206
+ ### Other bindings
207
+
208
+ - [vanilla-virtua](https://github.com/aabccd021/vanilla-virtua): virtua for vanilla js
209
+
210
+ ## Usage
211
+
212
+ The examples are written in React, but the same components and props are available in all frameworks.
213
+
214
+ ### Horizontal scroll
97
215
 
98
216
  ```tsx
99
217
  import { VList } from "virtua";
100
218
 
219
+ const sizes = [20, 40, 80, 77];
220
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
221
+ id: i,
222
+ size: sizes[i % 4],
223
+ }));
224
+
101
225
  export const App = () => {
102
226
  return (
103
- <VList style={{ height: 400 }} horizontal>
104
- {Array.from({ length: 1000 }).map((_, i) => (
227
+ <VList data={data} style={{ height: 400 }} horizontal>
228
+ {(item) => (
105
229
  <div
106
- key={i}
230
+ key={item.id}
107
231
  style={{
108
- width: Math.floor(Math.random() * 10) * 10 + 10,
109
- borderRight: "solid 1px gray",
232
+ width: item.size,
110
233
  background: "white",
234
+ borderRight: "solid 1px #ccc",
111
235
  }}
112
236
  >
113
- {i}
237
+ {item.id}
114
238
  </div>
115
- ))}
239
+ )}
116
240
  </VList>
117
241
  );
118
242
  };
119
243
  ```
120
244
 
121
- #### Customization
245
+ ### Custom scroll container
122
246
 
123
247
  `VList` is a recommended solution which works like a drop-in replacement of simple list built with scrollable `div` (or removed [virtual-scroller element](https://github.com/WICG/virtual-scroller)). For more complicated styling or markup, use `Virtualizer`.
124
248
 
125
249
  ```tsx
126
250
  import { Virtualizer } from "virtua";
127
251
 
252
+ const sizes = [20, 40, 80, 77];
253
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
254
+ id: i,
255
+ size: sizes[i % 4],
256
+ }));
257
+ const headerHeight = 40;
258
+
128
259
  export const App = () => {
129
260
  return (
130
- <div style={{ overflowY: "auto", overflowAnchor: "none", height: 800 }}>
131
- <div style={{ height: 40 }}>header</div>
132
- <Virtualizer startMargin={40}>
133
- {Array.from({ length: 1000 }).map((_, i) => (
261
+ <div
262
+ style={{
263
+ height: 800,
264
+ overflowY: "auto",
265
+ // opt out browser's scroll anchoring on header/footer because it will conflict with scroll anchoring of virtualizer
266
+ overflowAnchor: "none",
267
+ }}
268
+ >
269
+ <div style={{ height: headerHeight }}>header</div>
270
+ <Virtualizer data={data} startMargin={headerHeight}>
271
+ {(item) => (
134
272
  <div
135
- key={i}
273
+ key={item.id}
136
274
  style={{
137
- height: Math.floor(Math.random() * 10) * 10 + 10,
138
- borderBottom: "solid 1px gray",
275
+ height: item.size,
139
276
  background: "white",
277
+ borderBottom: "solid 1px #ccc",
140
278
  }}
141
279
  >
142
- {i}
280
+ {item.id}
143
281
  </div>
144
- ))}
282
+ )}
145
283
  </Virtualizer>
284
+ <div style={{ height: 600 }}>footer</div>
146
285
  </div>
147
286
  );
148
287
  };
149
288
  ```
150
289
 
151
- #### Window scroll
290
+ ### Window scroll
152
291
 
153
292
  ```tsx
154
293
  import { WindowVirtualizer } from "virtua";
155
294
 
295
+ const sizes = [20, 40, 80, 77];
296
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
297
+ id: i,
298
+ size: sizes[i % 4],
299
+ }));
300
+
156
301
  export const App = () => {
157
302
  return (
158
303
  <div style={{ padding: 200 }}>
159
- <WindowVirtualizer>
160
- {Array.from({ length: 1000 }).map((_, i) => (
304
+ <WindowVirtualizer data={data}>
305
+ {(item) => (
161
306
  <div
162
- key={i}
307
+ key={item.id}
163
308
  style={{
164
- height: Math.floor(Math.random() * 10) * 10 + 10,
165
- borderBottom: "solid 1px gray",
309
+ height: item.size,
166
310
  background: "white",
311
+ borderBottom: "solid 1px #ccc",
167
312
  }}
168
313
  >
169
- {i}
314
+ {item.id}
170
315
  </div>
171
- ))}
316
+ )}
172
317
  </WindowVirtualizer>
173
318
  </div>
174
319
  );
175
320
  };
176
321
  ```
177
322
 
178
- #### Tabular data
323
+ ### Tabular data
324
+
325
+ For data with rows and columns such as a table, use `VGrid`. It virtualizes on both axes.
179
326
 
180
327
  ```tsx
181
328
  import { VGrid } from "virtua";
@@ -189,32 +336,38 @@ const columns = [
189
336
  { key: "bio", width: 480 },
190
337
  ] as const;
191
338
 
192
- const rows = Array.from({ length: 10000 }, (_, i) => ({
193
- id: i,
194
- name: `User ${i}`,
195
- email: `user${i}@example.com`,
196
- age: 20 + (i % 50),
197
- joined: new Date(2020, 0, 1 + i).toDateString(),
198
- bio: `Hello, I'm user ${i}.`,
199
- }));
339
+ const rows = [
340
+ // the header row has no data
341
+ null,
342
+ ...Array.from({ length: 10000 }).map((_, i) => ({
343
+ id: i,
344
+ name: `User ${i}`,
345
+ email: `user${i}@example.com`,
346
+ age: 20 + (i % 50),
347
+ joined: new Date(2020, 0, 1 + i).toDateString(),
348
+ bio: `Hello, I'm user ${i}.`,
349
+ })),
350
+ ];
200
351
 
201
352
  export const App = () => {
202
353
  return (
203
354
  <VGrid
204
- style={{ height: 800, border: "solid 1px gray", background: "white" }}
355
+ style={{ height: 800, border: "solid 1px #ccc", background: "white" }}
205
356
  rows={rows}
206
357
  rowHeight={40}
207
358
  cols={columns}
208
359
  colWidth="width"
360
+ headerRows={1}
209
361
  >
210
362
  {(row, col) => (
211
363
  <div
212
364
  style={{
213
- borderRight: "solid 1px gray",
214
- borderBottom: "solid 1px gray",
365
+ borderRight: "solid 1px #ccc",
366
+ borderBottom: "solid 1px #ccc",
367
+ background: row === null ? "lightgray" : undefined,
215
368
  }}
216
369
  >
217
- {row[col.key]}
370
+ {row === null ? col.key : row[col.key]}
218
371
  </div>
219
372
  )}
220
373
  </VGrid>
@@ -222,133 +375,47 @@ export const App = () => {
222
375
  };
223
376
  ```
224
377
 
225
- ### Vue
226
-
227
- `vue >= 3.2` is required.
378
+ ### Masonry
228
379
 
229
- ```vue
230
- <script setup>
231
- import { VList } from "virtua/vue";
232
-
233
- const sizes = [20, 40, 180, 77];
234
- const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]);
235
- </script>
236
-
237
- <template>
238
- <VList :data="data" :style="{ height: '800px' }" #default="{ item, index }">
239
- <div
240
- :key="index"
241
- :style="{
242
- height: item + 'px',
243
- borderBottom: 'solid 1px #ccc',
244
- background: 'white',
245
- }"
246
- >
247
- {{ index }}
248
- </div>
249
- </VList>
250
- </template>
251
- ```
252
-
253
- ### Solid
254
-
255
- `solid-js >= 1.0` is required.
380
+ For items with different heights laid out in columns such as a gallery, use `VMasonry`.
256
381
 
257
382
  ```tsx
258
- import { VList } from "virtua/solid";
383
+ import { VMasonry } from "virtua";
259
384
 
260
- export const App = () => {
261
- const sizes = [20, 40, 180, 77];
262
- const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]);
385
+ const sizes = [100, 180, 140, 220];
386
+ const colors = ["skyblue", "pink", "khaki", "lightgreen", "plum"];
387
+ const data = Array.from({ length: 1000 }).map((_, i) => ({
388
+ id: i,
389
+ size: sizes[i % 4],
390
+ color: colors[i % 5],
391
+ }));
263
392
 
393
+ export const App = () => {
264
394
  return (
265
- <VList data={data} style={{ height: "800px" }}>
266
- {(d, i) => (
395
+ <VMasonry data={data} lanes={3} gap={8} style={{ height: 800 }}>
396
+ {(item) => (
267
397
  <div
398
+ key={item.id}
268
399
  style={{
269
- height: d + "px",
270
- "border-bottom": "solid 1px #ccc",
271
- background: "white",
400
+ height: item.size,
401
+ background: item.color,
272
402
  }}
273
403
  >
274
- {i()}
404
+ {item.id}
275
405
  </div>
276
406
  )}
277
- </VList>
407
+ </VMasonry>
278
408
  );
279
409
  };
280
410
  ```
281
411
 
282
- ### Svelte
283
-
284
- `svelte >= 5.0` is required.
285
-
286
- ```svelte
287
- <script lang="ts">
288
- import { VList } from "virtua/svelte";
412
+ ### More examples
289
413
 
290
- const sizes = [20, 40, 180, 77];
291
-
292
- const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]);
293
- </script>
294
-
295
- <VList {data} style="height: 800px;" getKey={(_, i) => i}>
296
- {#snippet children(item, index)}
297
- <div
298
- style="
299
- height: {item}px;
300
- border-bottom: solid 1px #ccc;
301
- background: white;
302
- "
303
- >
304
- {index}
305
- </div>
306
- {/snippet}
307
- </VList>
308
- ```
309
-
310
- ### Angular
311
-
312
- `@angular/core >= 20` is required. The components are zoneless-ready and work with both zone.js and [`provideZonelessChangeDetection`](https://angular.dev/api/core/provideZonelessChangeDetection).
313
-
314
- ```ts
315
- import { Component } from "@angular/core";
316
- import { VList } from "virtua/angular";
317
-
318
- const sizes = [20, 40, 180, 77];
319
-
320
- @Component({
321
- selector: "app-root",
322
- imports: [VList],
323
- template: `
324
- <virtua-vlist [data]="data" [getKey]="getKey" style="height: 800px;">
325
- <ng-template let-item let-index="index">
326
- <div
327
- [style.height.px]="item"
328
- style="background: white; border-bottom: solid 1px #ccc;"
329
- >
330
- {{ index }}
331
- </div>
332
- </ng-template>
333
- </virtua-vlist>
334
- `,
335
- })
336
- export class App {
337
- protected readonly data = Array.from({ length: 1000 }).map(
338
- (_, i) => sizes[i % 4]!,
339
- );
340
- protected readonly getKey = (_: number, i: number) => i;
341
- }
342
- ```
343
-
344
- ### Other bindings
345
-
346
- - [vanilla-virtua](https://github.com/aabccd021/vanilla-virtua): virtua for vanilla js
414
+ See [demo](https://inokawa.github.io/virtua/) and [its source code](./stories).
347
415
 
348
416
  ## Documentation
349
417
 
350
418
  - [API reference](./docs/API.md)
351
- - [Storybook examples](./stories) for more usages
352
419
  - [DeepWiki](https://deepwiki.com/inokawa/virtua)
353
420
 
354
421
  ### FAQs
@@ -408,11 +475,17 @@ It may be dispatched by ResizeObserver in this lib [as described in spec](https:
408
475
 
409
476
  Especially for `webpack-dev-server`, [you can filter out the specific error with `devServer.client.overlay.runtimeErrors` option](https://webpack.js.org/configuration/dev-server/#overlay).
410
477
 
411
- #### Why my items are squashed(or rendered inconsistently) on resize/add/remove?
478
+ #### Why are my items squashed, overlapped or rendered inconsistently on resize/add/remove/reorder?
412
479
 
413
- Maybe you forgot to pass `key` prop to each items, or the keys are not unique. Item sizes are stored per key.
480
+ Check that each item has a unique key, such as the id of your data, not its index.
414
481
 
415
- And do not use index of items as `key`, especially when you want to toggle `shift` prop to `true`. Prepending will increment every indexes of items and that will cause unexpected behavior.
482
+ - React: `key` of the element of each item
483
+ - Vue: `key` of the root element in the default slot
484
+ - Solid: the item of `data` itself, so keep the same reference for the same item
485
+ - Svelte: `getKey` prop
486
+ - Angular: `getKey` input
487
+
488
+ If it still happens, the `shift` prop may be misused.
416
489
 
417
490
  #### Why `VListHandle.viewportSize` is 0 on mount?
418
491
 
@@ -426,32 +499,52 @@ This package uses [exports of package.json](https://nodejs.org/api/packages.html
426
499
 
427
500
  ### Features
428
501
 
429
- | | [virtua](https://github.com/inokawa/virtua) | [react-virtuoso](https://github.com/petyosi/react-virtuoso) | [react-window](https://github.com/bvaughn/react-window) | [react-virtualized](https://github.com/bvaughn/react-virtualized) | [@tanstack/react-virtual](https://github.com/TanStack/virtual) |
430
- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
431
- | Bundle size | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/virtua)](https://bundlephobia.com/package/virtua) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-virtuoso)](https://bundlephobia.com/package/react-virtuoso) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-window)](https://bundlephobia.com/package/react-window) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-virtualized)](https://bundlephobia.com/package/react-virtualized) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/react-virtual)](https://bundlephobia.com/package/@tanstack/react-virtual) |
432
- | Vertical scroll | ✅ | ✅ | ✅ | ✅ | 🟠 (needs customization) |
433
- | Horizontal scroll | ✅ | ✅ | ✅ | ✅ | 🟠 (needs customization) |
434
- | Horizontal scroll in RTL direction | ✅ | ❌ | ✅ | ❌ | 🟠 (isRtl) |
435
- | Grid (Virtualization for two dimensions) | ✅ (VGrid) | ❌ | ✅ ([Grid](https://react-window.vercel.app/grid/grid)) | ✅ ([Grid](https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md)) | 🟠 (needs customization) |
436
- | Tabular data (Columns with headers) | ✅ (VGrid) | ✅ (TableVirtuoso) | ✅ ([Supported](https://react-window.vercel.app/list/tabular-data)) | ✅ ([Table](https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md)) | 🟠 (needs customization) |
437
- | HTML table element | 🟠 (needs customization) | ✅ (TableVirtuoso) | ❌ | ❌ | 🟠 (needs customization) |
438
- | Masonry | ❌ | ✅ (VirtuosoMasonry) | ❌ | ✅ ([Masonry](https://github.com/bvaughn/react-virtualized/blob/master/docs/Masonry.md)) | 🟠 (needs customization) |
439
- | Window scroller | ✅ (WindowVirtualizer) | ✅ | ❌ | ✅ ([WindowScroller](https://github.com/bvaughn/react-virtualized/blob/master/docs/WindowScroller.md)) | ✅ (useWindowVirtualizer) |
440
- | Dynamic list size | ✅ | ✅ | ✅ | 🟠 (needs [AutoSizer](https://github.com/bvaughn/react-virtualized/blob/master/docs/AutoSizer.md)) | ✅ |
441
- | Dynamic item size | ✅ | ✅ | 🟠 (needs additional codes and has wrong destination when scrolling to item imperatively) | 🟠 (needs [CellMeasurer](https://github.com/bvaughn/react-virtualized/blob/master/docs/CellMeasurer.md) and has wrong destination when scrolling to item imperatively) | ✅ |
442
- | Reverse scroll | ✅ | ✅ | ❌ | ❌ | ❌ |
443
- | Reverse scroll in iOS Safari | 🟠 ([user must release scroll](https://github.com/inokawa/virtua/issues/473)) | 🟠 ([has glitch with unknown sized items](https://github.com/petyosi/react-virtuoso/issues/945)) | ❌ | ❌ | ❌ |
444
- | Infinite scroll | ✅ | ✅ | 🟠 (needs [react-window-infinite-loader](https://github.com/bvaughn/react-window-infinite-loader)) | 🟠 (needs [InfiniteLoader](https://github.com/bvaughn/react-virtualized/blob/master/docs/InfiniteLoader.md)) | ✅ |
445
- | Reverse (bi-directional) infinite scroll | ✅ | ✅ | ❌ | ❌ | ❌ |
446
- | Scroll restoration | ✅ | ✅ (getState) | ❌ | ❌ | ❌ |
447
- | Smooth scroll | ✅ | ✅ | ❌ | ❌ | ✅ |
448
- | SSR support | ✅ | ✅ | ✅ | ✅ | ✅ |
449
- | Render React Server Components (RSC) as children | ✅ | ❌ | ❌ | ❌ | 🟠(needs customization) |
450
- | Display exceeding [browser's max element size](https://stackoverflow.com/questions/10882769/do-the-browsers-have-a-maximum-height-for-the-body-document) limit | ❌ | ❌ | ❌ | ✅ | ❌ |
502
+ | | [virtua](https://github.com/inokawa/virtua) | [react-virtuoso](https://github.com/petyosi/react-virtuoso) | [react-window](https://github.com/bvaughn/react-window) | [@tanstack/react-virtual](https://github.com/TanStack/virtual) | [react-virtualized](https://github.com/bvaughn/react-virtualized) |
503
+ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
504
+ | Bundle size | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/virtua)](https://bundlephobia.com/package/virtua) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-virtuoso)](https://bundlephobia.com/package/react-virtuoso) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-window)](https://bundlephobia.com/package/react-window) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/react-virtual)](https://bundlephobia.com/package/@tanstack/react-virtual) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-virtualized)](https://bundlephobia.com/package/react-virtualized) |
505
+ | Vertical scroll | ✅ | ✅ | ✅ | 🟠 (needs customization) | ✅ |
506
+ | Horizontal scroll | ✅ | ✅ | ✅ | 🟠 (needs customization) | ✅ |
507
+ | Horizontal scroll in RTL direction | ✅ | ✅ | ✅ | 🟠 (isRtl) | ❌ |
508
+ | Grid (Virtualization for two dimensions) | ✅ (VGrid) | ❌ | ✅ ([Grid](https://react-window.vercel.app/grid/grid)) | 🟠 (needs customization) | ✅ ([Grid](https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md)) |
509
+ | Tabular data (Columns with headers) | ✅ (VGrid) | ✅ (TableVirtuoso) | ✅ ([Supported](https://react-window.vercel.app/list/tabular-data)) | 🟠 (needs customization) | ✅ ([Table](https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md)) |
510
+ | HTML table element | 🟠 (needs customization) | ✅ (TableVirtuoso) | ❌ | 🟠 (needs customization) | ❌ |
511
+ | Masonry | ✅ (VMasonry) | ✅ (VirtuosoMasonry) | ❌ | 🟠 (lanes) | ✅ ([Masonry](https://github.com/bvaughn/react-virtualized/blob/master/docs/Masonry.md)) |
512
+ | Window scroller | ✅ (WindowVirtualizer) | ✅ | ❌ | ✅ (useWindowVirtualizer) | ✅ ([WindowScroller](https://github.com/bvaughn/react-virtualized/blob/master/docs/WindowScroller.md)) |
513
+ | Dynamic list size | ✅ | ✅ | ✅ | ✅ | 🟠 (needs [AutoSizer](https://github.com/bvaughn/react-virtualized/blob/master/docs/AutoSizer.md)) |
514
+ | Dynamic item size | ✅ | ✅ | ✅ (useDynamicRowHeight) | ✅ (measureElement) | 🟠 (needs [CellMeasurer](https://github.com/bvaughn/react-virtualized/blob/master/docs/CellMeasurer.md) and has wrong destination when scrolling to item imperatively) |
515
+ | Reverse scroll | ✅ | ✅ | ❌ | ✅ (anchorTo) | ❌ |
516
+ | Reverse scroll in iOS Safari | 🟠 ([user must release scroll](https://github.com/inokawa/virtua/issues/473)) | 🟠 ([has glitch with unknown sized items](https://github.com/petyosi/react-virtuoso/issues/945)) | ❌ | 🟠 | ❌ |
517
+ | Infinite scroll | ✅ | ✅ | 🟠 (needs [react-window-infinite-loader](https://github.com/bvaughn/react-window-infinite-loader)) | ✅ | 🟠 (needs [InfiniteLoader](https://github.com/bvaughn/react-virtualized/blob/master/docs/InfiniteLoader.md)) |
518
+ | Reverse (bi-directional) infinite scroll | ✅ | ✅ | ❌ | ✅ (anchorTo) | ❌ |
519
+ | Scroll restoration | ✅ | ✅ (getState) | ❌ | ✅ (takeSnapshot) | ❌ |
520
+ | Smooth scroll | ✅ | ✅ | 🟠 ([not with useDynamicRowHeight](https://react-window.vercel.app/list/dynamic-row-height)) | ✅ | ❌ |
521
+ | SSR support | ✅ (ssrCount) | ✅ (initialItemCount) | ✅ (defaultHeight) | ✅ (initialRect) | ✅ |
522
+ | Render React Server Components (RSC) as children | ✅ | ❌ | ❌ | 🟠 (needs customization) | ❌ |
523
+ | Display exceeding [browser's max element size](https://stackoverflow.com/questions/10882769/do-the-browsers-have-a-maximum-height-for-the-body-document) limit | ❌ | ❌ | ❌ | ❌ | ✅ |
524
+
525
+ - ✅ - Built-in supported
526
+ - 🟠 - Supported but partial, limited or requires some user custom code
527
+ - ❌ - Not officially supported
528
+
529
+ ### Grid features
530
+
531
+ | | [virtua](https://github.com/inokawa/virtua) (VGrid) | [AG Grid Community](https://www.ag-grid.com/react-data-grid/) | [MUI X Data Grid](https://mui.com/x/react-data-grid/) | [react-data-grid](https://github.com/Comcast/react-data-grid) | [@virtuoso.dev/data-table](https://virtuoso.dev/data-table/) | [react-window](https://github.com/bvaughn/react-window) (Grid) | [@tanstack/react-virtual](https://github.com/TanStack/virtual) |
532
+ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
533
+ | Bundle size | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/virtua)](https://bundlephobia.com/package/virtua) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/ag-grid-react)](https://bundlephobia.com/package/ag-grid-react) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@mui/x-data-grid)](https://bundlephobia.com/package/@mui/x-data-grid) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-data-grid)](https://bundlephobia.com/package/react-data-grid) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@virtuoso.dev/data-table)](https://bundlephobia.com/package/@virtuoso.dev/data-table) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-window)](https://bundlephobia.com/package/react-window) | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/react-virtual)](https://bundlephobia.com/package/@tanstack/react-virtual) |
534
+ | Row / column virtualization | ✅ | ✅ | 🟠 (rows are limited to 100 per page, 💰 [Pro](https://mui.com/x/react-data-grid/virtualization/#row-virtualization) for more) | ✅ | ✅ | ✅ | 🟠 (needs customization) |
535
+ | Dynamic row height / column width | ✅ (auto) | ✅ ([autoHeight](https://www.ag-grid.com/react-data-grid/row-height/#auto-row-height) / [autoSizeStrategy](https://www.ag-grid.com/react-data-grid/column-sizing/#continuous-auto-sizing)) | 🟠 (only row height, column width needs [autosizing on demand](https://mui.com/x/react-data-grid/column-dimensions/#autosizing)) | 🟠 (only column width, measured once) | 🟠 (only row height, column width is [measured from header cells](https://virtuoso.dev/data-table/columns/column-layout/)) | ❌ | 🟠 (measureElement) |
536
+ | Pinned rows / columns | ✅ | ✅ | 💰 (Pro, [rows](https://mui.com/x/react-data-grid/row-pinning/) / [columns](https://mui.com/x/react-data-grid/column-pinning/)) | ✅ | 🟠 (only columns) | ❌ | 🟠 (rangeExtractor) |
537
+ | Row / column spanning | ✅ | ✅ | 🟠 ([row spanning needs fixed row height](https://mui.com/x/react-data-grid/row-spanning/)) | 🟠 (only columns) | 🟠 (only [column group headers](https://virtuoso.dev/data-table/columns/column-groups/)) | ❌ | ❌ |
538
+ | Sticky group rows | ✅ (sectionRows) | 💰 ([Enterprise](https://www.ag-grid.com/react-data-grid/grouping-opening-groups/)) | ❌ | ❌ | ✅ ([Grouped rows](https://virtuoso.dev/data-table/grouped-rows/)) | ❌ | 🟠 (rangeExtractor) |
539
+ | SSR support | ❌ | ❌ | ❌ | 🟠 (limited to the first few rows) | ❌ | ✅ (defaultHeight, defaultWidth) | ✅ (initialRect) |
540
+ | Window scroller | ❌ | ❌ | ❌ | ❌ | ✅ (useWindowScroll) | ❌ | ✅ (useWindowVirtualizer) |
541
+ | Display exceeding [browser's max element size](https://stackoverflow.com/questions/10882769/do-the-browsers-have-a-maximum-height-for-the-body-document) limit | ❌ | ✅ ([Stretching](https://www.ag-grid.com/react-data-grid/massive-row-count/)) | ❌ | ❌ | ❌ | ❌ | ❌ |
542
+ | Use with [TanStack Table](https://github.com/TanStack/table) | ✅ ([example](./stories/react/advanced/With%20tanstack-table.stories.tsx)) | ❌ | ❌ | ❌ | ❌ | 🟠 (needs customization) | ✅ ([guide](https://tanstack.com/table/latest/docs/framework/react/guide/virtualization)) |
451
543
 
452
544
  - ✅ - Built-in supported
453
545
  - 🟠 - Supported but partial, limited or requires some user custom code
454
546
  - ❌ - Not officially supported
547
+ - 💰 - Supported only in paid plans
455
548
 
456
549
  ### Benchmark
457
550