vue-virtual-flow 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A small, typed, and accessible collection of virtual scrolling components for
6
6
  Vue 3. It covers fixed and variable-height lists, window scrolling, chat,
7
- short-media feeds, and horizontal carousels.
7
+ window-scrolling grids, short-media feeds, and horizontal carousels.
8
8
 
9
9
  Only the items needed for the current viewport are mounted, keeping large data
10
10
  sets responsive without prescribing how an item should look.
@@ -18,9 +18,11 @@ sets responsive without prescribing how an item should look.
18
18
  - [VirtualList and DynamicVirtualScroll](#virtuallist-and-dynamicvirtualscroll)
19
19
  - [VirtualScroll](#virtualscroll)
20
20
  - [WindowDynamicVirtualScroll](#windowdynamicvirtualscroll)
21
+ - [WindowGirdVirtualScroll](#windowgirdvirtualscroll)
21
22
  - [ChatVirtualScroll](#chatvirtualscroll)
22
23
  - [ShortMediaFeed](#shortmediafeed)
23
24
  - [VirtualCarousel](#virtualcarousel)
25
+ - [SkeletonLoader](#skeletonloader)
24
26
  - [Shared recipes](#shared-recipes)
25
27
  - [Viewport height](#viewport-height)
26
28
  - [Stable item keys](#stable-item-keys)
@@ -43,10 +45,19 @@ Import the library stylesheet once in your application entry point:
43
45
  import 'vue-virtual-flow/style.css'
44
46
  ```
45
47
 
46
- The package requires Vue `^3.4.0` and Node.js `>=18`.
48
+ | Dependency | Requirement |
49
+ | --- | --- |
50
+ | Vue | `^3.4.0` |
51
+ | Node.js | `>=18` |
47
52
 
48
53
  ## Quick start
49
54
 
55
+ | Registration | Usage |
56
+ | --- | --- |
57
+ | Local imports | `import { VirtualList, SkeletonLoader } from 'vue-virtual-flow'` |
58
+ | Global plugin | `createApp(App).use(VueVirtualScroll)` |
59
+ | Shared styles | `import 'vue-virtual-flow/style.css'` |
60
+
50
61
  Import components locally:
51
62
 
52
63
  ```vue
@@ -83,8 +94,8 @@ createApp(App).use(VueVirtualScroll).mount('#app')
83
94
  ```
84
95
 
85
96
  The plugin registers `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
86
- `WindowDynamicVirtualScroll`, `ChatVirtualScroll`, `ShortMediaFeed`, and
87
- `VirtualCarousel`.
97
+ `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`, `ChatVirtualScroll`,
98
+ `ShortMediaFeed`, `VirtualCarousel`, and `SkeletonLoader`.
88
99
 
89
100
  ## Choose a component
90
101
 
@@ -94,12 +105,26 @@ The plugin registers `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
94
105
  | `DynamicVirtualScroll` | Same behavior as `VirtualList`, with an explicit name. | Component container | Variable, measured automatically |
95
106
  | `VirtualScroll` | Every row has the same known height. | Component container | Fixed |
96
107
  | `WindowDynamicVirtualScroll` | The page itself should scroll instead of a nested container. | Browser window | Variable, measured automatically |
108
+ | `WindowGirdVirtualScroll` | A fixed-height card grid should use page scrolling. | Browser window | Fixed row height |
97
109
  | `ChatVirtualScroll` | Messages are appended at the bottom and older history is prepended. | Component container | Variable, measured automatically |
98
110
  | `ShortMediaFeed` | One full-height item should snap into view at a time. | Component container | One viewport per item |
99
111
  | `VirtualCarousel` | Several horizontal slides should snap and virtualize. | Component container | Calculated from container width |
112
+ | `SkeletonLoader` | You need a placeholder while content loads. | None | Configurable width and height |
100
113
 
101
114
  ## Component guides
102
115
 
116
+ Events use Vue template listeners; action methods are called through a component
117
+ ref. The tables below use `list`, `chat`, `feed`, or `carousel` as the ref name.
118
+
119
+ | Task | Usage |
120
+ | --- | --- |
121
+ | Declare a typed ref | `const list = ref<VirtualScrollExpose>()` |
122
+ | Attach the ref to the component | `<VirtualScroll ref="list" ... />` |
123
+ | Call after mount in a script | `list.value?.scrollToIndex(10)` |
124
+ | Call from a template event | `@click="list?.scrollToIndex(10)"` |
125
+ | Select scroll behavior | Pass `{ behavior: 'smooth' }` to `scrollTo` / `scrollToIndex`, or `'smooth'` to `scrollToTop`, `scrollToBottom`, `next`, or `previous`. |
126
+ | Align a list item | `scrollToIndex(index, { align: 'start' })` / `'center'` / `'end'` |
127
+
103
128
  ### VirtualList and DynamicVirtualScroll
104
129
 
105
130
  `VirtualList` is an alias of `DynamicVirtualScroll`. Both names render the same
@@ -197,19 +222,19 @@ to items that have not been measured.
197
222
 
198
223
  #### Events
199
224
 
200
- | Event | Payload | When it fires |
201
- | --- | --- | --- |
202
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. |
203
- | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. |
204
- | `refresh` | none | After pulling past the refresh threshold and releasing. |
225
+ | Event | Payload | When it fires | Usage |
226
+ | --- | --- | --- | --- |
227
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. | `@scroll="onScroll"` |
228
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
229
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
205
230
 
206
231
  #### Exposed API
207
232
 
208
- | Method | Description |
209
- | --- | --- |
210
- | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. |
211
- | `scrollToIndex(index, { align?, behavior? })` | Scrolls to a zero-based index. `align` is `start`, `center`, or `end`. |
212
- | `scrollToTop(behavior?)` | Scrolls to the start of the list. |
233
+ | Method / state | Description | Template example |
234
+ | --- | --- | --- |
235
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. | `list?.scrollTo(200, { behavior: 'smooth' })` |
236
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
237
+ | `scrollToTop(behavior?)` | Scrolls to the start of the list. | `list?.scrollToTop()` |
213
238
 
214
239
  Long-distance smooth jumps automatically become immediate jumps because
215
240
  unmeasured rows can change the target offset while scrolling.
@@ -283,13 +308,30 @@ The rendered row must stay at `itemSize`. If content can wrap or resize, use
283
308
  | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
284
309
  | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
285
310
 
286
- #### Slots, events, and exposed API
311
+ #### Slots
312
+
313
+ | Slot | Scope | Description |
314
+ | --- | --- | --- |
315
+ | `default` | `{ item, index }` | Renders each mounted row. |
316
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
317
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
318
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
319
+
320
+ #### Events
321
+
322
+ | Event | Payload | When it fires | Usage |
323
+ | --- | --- | --- | --- |
324
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. `endIndex` is exclusive. | `@scroll="onScroll"` |
325
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
326
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
327
+
328
+ #### Exposed API
287
329
 
288
- - Slots: `default({ item, index })`, `empty`, `loading`, and
289
- `refresh({ pullDistance, progress, refreshing })`.
290
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more`, and
291
- `refresh`.
292
- - Methods: `scrollTo`, `scrollToIndex`, and `scrollToTop`.
330
+ | Method / state | Description | Template example |
331
+ | --- | --- | --- |
332
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset inside the list. | `list?.scrollTo(200, { behavior: 'smooth' })` |
333
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
334
+ | `scrollToTop(behavior?)` | Scrolls to the start of the list. | `list?.scrollToTop()` |
293
335
 
294
336
  ### WindowDynamicVirtualScroll
295
337
 
@@ -353,14 +395,111 @@ const list = ref<VirtualScrollExpose>()
353
395
  | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
354
396
  | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
355
397
 
356
- #### Slots, events, and exposed API
398
+ #### Slots
399
+
400
+ | Slot | Scope | Description |
401
+ | --- | --- | --- |
402
+ | `default` | `{ item, index }` | Renders each mounted row. |
403
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
404
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
405
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
406
+
407
+ #### Events
408
+
409
+ | Event | Payload | When it fires | Usage |
410
+ | --- | --- | --- | --- |
411
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On window scroll. `scrollTop` is relative to the component start. `endIndex` is exclusive. | `@scroll="onScroll"` |
412
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
413
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
414
+
415
+ #### Exposed API
416
+
417
+ | Method / state | Description | Template example |
418
+ | --- | --- | --- |
419
+ | `scrollTo(position, options?)` | Scrolls the window to a pixel offset relative to the list start. | `list?.scrollTo(200, { behavior: 'smooth' })` |
420
+ | `scrollToIndex(index, options?)` | Scrolls the window to a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
421
+ | `scrollToTop(behavior?)` | Scrolls the window to the component start. | `list?.scrollToTop()` |
422
+
423
+ ### WindowGirdVirtualScroll
424
+
425
+ Use `WindowGirdVirtualScroll` for a multi-column grid whose cards have a known,
426
+ fixed height and whose viewport is the browser window. Virtualization happens
427
+ by complete rows, so every visible column remains mounted together.
428
+
429
+ #### Example
430
+
431
+ ```vue
432
+ <script setup lang="ts">
433
+ import { WindowGirdVirtualScroll } from 'vue-virtual-flow'
434
+
435
+ const products = Array.from({ length: 10_000 }, (_, id) => ({
436
+ id,
437
+ name: `Product ${id + 1}`,
438
+ }))
439
+ </script>
440
+
441
+ <template>
442
+ <WindowGirdVirtualScroll
443
+ :items="products"
444
+ :item-size="240"
445
+ :columns="4"
446
+ :gap="16"
447
+ item-key="id"
448
+ aria-label="Product grid"
449
+ >
450
+ <template #default="{ item, rowIndex, columnIndex }">
451
+ <ProductCard
452
+ :product="item"
453
+ :data-position="`${rowIndex}:${columnIndex}`"
454
+ />
455
+ </template>
456
+ </WindowGirdVirtualScroll>
457
+ </template>
458
+ ```
459
+
460
+ #### Props
461
+
462
+ | Prop | Type | Default | Description |
463
+ | --- | --- | --- | --- |
464
+ | `items` | `readonly T[]` | required | Data passed to the default slot. |
465
+ | `itemSize` | `number` | required | Fixed height of each grid row in pixels. Overflow is clipped. |
466
+ | `columns` | `number` | `2` | Number of columns in each row. |
467
+ | `gap` | `number` | `0` | Spacing between rows and columns in pixels. |
468
+ | `overscan` | `number` | `1` | Extra rows mounted before and after the visible range. |
469
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key`, or index | Stable identity for each item. |
470
+ | `ariaLabel` | `string` | `"Window virtual grid"` | Accessible label for the grid. |
471
+ | `hasMore` | `boolean` | `false` | Indicates that another page can be loaded. |
472
+ | `loading` | `boolean` | `false` | Prevents duplicate requests and shows the loading row. |
473
+ | `loadingItemSize` | `number` | `itemSize` | Reserved loading-row height in pixels. |
474
+ | `loadMoreThreshold` | `number` | `200` | Distance from the window bottom to the grid end that triggers `load-more`, in pixels. |
475
+ | `pullToRefresh` | `boolean` | `false` | Enables refresh while the page is at `window.scrollY = 0`. |
476
+ | `refreshing` | `boolean` | `false` | Keeps the refresh indicator open during async work. |
477
+ | `pullRefreshThreshold` | `number` | `64` | Pull distance required to emit `refresh`, in pixels. |
478
+
479
+ #### Slots
480
+
481
+ | Slot | Scope | Description |
482
+ | --- | --- | --- |
483
+ | `default` | `{ item, index, rowIndex, columnIndex }` | Renders each mounted cell. |
484
+ | `empty` | none | Rendered when `items` is empty and `loading` is false. |
485
+ | `loading` | none | Loading row; defaults to “Loading more…”. |
486
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Pull-to-refresh indicator. `progress` is clamped from `0` to `1`. |
487
+
488
+ #### Events
489
+
490
+ | Event | Payload | When it fires | Usage |
491
+ | --- | --- | --- | --- |
492
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On window scroll. `scrollTop` is relative to the component start. `endIndex` is exclusive. | `@scroll="onScroll"` |
493
+ | `load-more` | none | Near the end when `hasMore` is true and `loading` is false. | `@load-more="loadMore"` |
494
+ | `refresh` | none | After pulling past the refresh threshold and releasing. | `@refresh="refreshFirstPage"` |
495
+
496
+ #### Exposed API
357
497
 
358
- - Slots: `default({ item, index })`, `empty`, `loading`, and
359
- `refresh({ pullDistance, progress, refreshing })`.
360
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more`, and
361
- `refresh`. `scrollTop` is relative to the component start.
362
- - Methods: `scrollTo`, `scrollToIndex`, and `scrollToTop`. They scroll the
363
- browser window; `scrollTo` accepts an offset relative to the list.
498
+ | Method / state | Description | Template example |
499
+ | --- | --- | --- |
500
+ | `scrollTo(position, options?)` | Scrolls the window to a pixel offset relative to the list start. | `list?.scrollTo(200, { behavior: 'smooth' })` |
501
+ | `scrollToIndex(index, options?)` | Scrolls the window to the row containing a zero-based item index. `align` is `start`, `center`, or `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
502
+ | `scrollToTop(behavior?)` | Scrolls the window to the component start. | `list?.scrollToTop()` |
364
503
 
365
504
  ### ChatVirtualScroll
366
505
 
@@ -451,21 +590,21 @@ height added above the viewport and restores the reader's position.
451
590
 
452
591
  #### Events
453
592
 
454
- | Event | Payload | When it fires |
455
- | --- | --- | --- |
456
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. |
457
- | `load-older` | none | Near the top when `hasOlder` is true and `loadingOlder` is false. |
458
- | `bottom-change` | `boolean` | When the viewport enters or leaves the bottom threshold. |
593
+ | Event | Payload | When it fires | Usage |
594
+ | --- | --- | --- | --- |
595
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | On container scroll. | `@scroll="onScroll"` |
596
+ | `load-older` | none | Near the top when `hasOlder` is true and `loadingOlder` is false. | `@load-older="loadOlder"` |
597
+ | `bottom-change` | `boolean` | When the viewport enters or leaves the bottom threshold. | `@bottom-change="onBottomChange"` |
459
598
 
460
599
  #### Exposed API
461
600
 
462
- | Member | Description |
463
- | --- | --- |
464
- | `isAtBottom` | Readonly boolean indicating whether the user is near the bottom. |
465
- | `scrollTo(position, options?)` | Scrolls to a pixel offset. |
466
- | `scrollToIndex(index, { align?, behavior? })` | Scrolls to a zero-based message index. |
467
- | `scrollToTop(behavior?)` | Scrolls to the oldest loaded message. |
468
- | `scrollToBottom(behavior?)` | Scrolls to the newest loaded message. |
601
+ | Method / state | Description | Template example |
602
+ | --- | --- | --- |
603
+ | `isAtBottom` | Readonly boolean indicating whether the user is near the bottom. | `chat?.isAtBottom` |
604
+ | `scrollTo(position, options?)` | Scrolls to a pixel offset. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
605
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based message index. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
606
+ | `scrollToTop(behavior?)` | Scrolls to the oldest loaded message. | `chat?.scrollToTop()` |
607
+ | `scrollToBottom(behavior?)` | Scrolls to the newest loaded message. | `chat?.scrollToBottom('smooth')` |
469
608
 
470
609
  ### ShortMediaFeed
471
610
 
@@ -533,23 +672,39 @@ keep persistent playback state in the parent.
533
672
  | `loading` | `boolean` | `false` | Prevents duplicate `load-more` requests. |
534
673
  | `loadMoreThreshold` | `number` | `2` | Remaining item count that triggers `load-more`. |
535
674
 
536
- #### Slots and events
675
+ #### Slots
676
+
677
+ | Slot | Scope | Description |
678
+ | --- | --- | --- |
679
+ | `default` | `{ item, index, active }` | Renders a mounted item; `active` marks the snapped item. |
680
+ | `empty` | none | Rendered when `items` is empty. |
681
+
682
+ #### Events
537
683
 
538
- | Kind | Name | Payload/scope | Description |
684
+ | Event | Payload | When it fires | Usage |
539
685
  | --- | --- | --- | --- |
540
- | Slot | `default` | `{ item, index, active }` | Renders a mounted item; `active` marks the snapped item. |
541
- | Slot | `empty` | none | Rendered when `items` is empty. |
542
- | Event | `update:activeIndex` | `number` | Supports `v-model:active-index`. |
543
- | Event | `change` | `{ index, item }` | The active item changes. |
544
- | Event | `reach-start` | none | Navigation changes to the first item. |
545
- | Event | `reach-end` | none | Navigation changes to the last item. |
546
- | Event | `load-more` | none | Remaining items reach `loadMoreThreshold`. |
686
+ | `update:activeIndex` | `number` | Supports `v-model:active-index`. | `v-model:active-index="activeIndex"` |
687
+ | `change` | `{ index, item }` | The active item changes. | `@change="onChange"` |
688
+ | `reach-start` | none | Navigation changes to the first item. | `@reach-start="onReachStart"` |
689
+ | `reach-end` | none | Navigation changes to the last item. | `@reach-end="onReachEnd"` |
690
+ | `load-more` | none | Remaining items reach `loadMoreThreshold`. | `@load-more="loadMore"` |
691
+
692
+ #### Exposed API
693
+
694
+ | Method / state | Description | Template example |
695
+ | --- | --- | --- |
696
+ | `scrollToIndex(index, options?)` | Scrolls to a zero-based item index using standard `ScrollToOptions`. | `feed?.scrollToIndex(10, { behavior: 'smooth' })` |
547
697
 
548
- #### Exposed API and keyboard controls
698
+ #### Keyboard controls
549
699
 
550
- `scrollToIndex(index, options?)` scrolls to an item using standard
551
- `ScrollToOptions`. The viewport supports Arrow Up/Down, Page Up/Down, Home,
552
- and End.
700
+ These keys work while the viewport is focused:
701
+
702
+ | Key | Action |
703
+ | --- | --- |
704
+ | `ArrowDown`, `PageDown` | Moves to the next item. |
705
+ | `ArrowUp`, `PageUp` | Moves to the previous item. |
706
+ | `Home` | Moves to the first position. |
707
+ | `End` | Moves to the last item. |
553
708
 
554
709
  For an unbounded feed, page data with `load-more` and keep a reasonable item
555
710
  window in the parent. Virtualization limits mounted DOM and media elements,
@@ -633,31 +788,74 @@ five slides are normally mounted in the middle of the collection.
633
788
  | `autoplayDelay` | `number` | `3000` | Delay in milliseconds between automatic transitions; values below `1` are clamped and non-finite values use the default. |
634
789
  | `autoplayLoop` | `boolean` | `true` | Returns to the first slide when reaching the end. |
635
790
  | `pauseOnHover` | `boolean` | `true` | Pauses autoplay when the carousel is hovered. |
791
+ | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Overrides slide count, scroll step, and gap at container-width breakpoints. |
792
+
793
+ #### Responsive configuration
794
+
795
+ The largest `breakpoint` not exceeding the carousel container width is used.
796
+ If no breakpoint matches, the base props apply. Omitted fields fall back to
797
+ the base props, not to another breakpoint.
798
+
799
+ | Field | Type | Default | Description |
800
+ | --- | --- | --- | --- |
801
+ | `breakpoint` | `number` | required | Minimum container width in pixels. |
802
+ | `slidesPerView` | `number` | `slidesPerView` | Slides visible at this breakpoint. |
803
+ | `slidesToScroll` | `number` | `slidesToScroll` | Slides moved by `next()` and `previous()`. |
804
+ | `gap` | `number` | `gap` | Space between slides in pixels. |
805
+
806
+ ```vue
807
+ <VirtualCarousel
808
+ :items="products"
809
+ :slides-per-view="1"
810
+ :responsive="[
811
+ { breakpoint: 640, slidesPerView: 2, gap: 12 },
812
+ { breakpoint: 1024, slidesPerView: 3, slidesToScroll: 2, gap: 16 },
813
+ ]"
814
+ >
815
+ <template #default="{ item }"><ProductCard :product="item" /></template>
816
+ </VirtualCarousel>
817
+ ```
818
+
819
+ #### Slots
636
820
 
637
- #### Slots and events
821
+ | Slot | Scope | Description |
822
+ | --- | --- | --- |
823
+ | `default` | `{ item, index, active, visible }` | Renders each mounted slide. |
824
+ | `empty` | none | Rendered when `items` is empty. |
825
+
826
+ #### Events
638
827
 
639
- | Kind | Name | Payload/scope | Description |
828
+ | Event | Payload | When it fires | Usage |
640
829
  | --- | --- | --- | --- |
641
- | Slot | `default` | `{ item, index, active, visible }` | Renders each mounted slide. |
642
- | Slot | `empty` | none | Rendered when `items` is empty. |
643
- | Event | `update:activeIndex` | `number` | Supports `v-model:active-index`. |
644
- | Event | `change` | `{ index, item }` | The first visible slide changes. |
645
- | Event | `reach-start` | none | Navigation changes to the first position. |
646
- | Event | `reach-end` | none | Navigation changes to the final valid position. |
647
- | Event | `load-more` | none | Remaining slides reach `loadMoreThreshold`. |
830
+ | `update:activeIndex` | `number` | Supports `v-model:active-index`. | `v-model:active-index="activeIndex"` |
831
+ | `change` | `{ index, item }` | The first visible slide changes. | `@change="onChange"` |
832
+ | `reach-start` | none | Navigation changes to the first position. | `@reach-start="onReachStart"` |
833
+ | `reach-end` | none | Navigation changes to the final valid position. | `@reach-end="onReachEnd"` |
834
+ | `load-more` | none | Remaining slides reach `loadMoreThreshold`. | `@load-more="loadMore"` |
835
+
836
+ #### Exposed API
648
837
 
649
- #### Exposed API and keyboard controls
838
+ | Method / state | Description | Template example |
839
+ | --- | --- | --- |
840
+ | `next(behavior?)` | Advances by `slidesToScroll`; defaults to smooth scrolling. | `carousel?.next()` |
841
+ | `previous(behavior?)` | Moves back by `slidesToScroll`. | `carousel?.previous()` |
842
+ | `scrollToIndex(index, options?)` | Makes the index the first visible slide. | `carousel?.scrollToIndex(10, { behavior: 'smooth' })` |
843
+ | `startAutoplay()` | Starts or restarts the timer when `autoplay` is true and items are available. | `carousel?.startAutoplay()` |
844
+ | `stopAutoplay()` | Clears the current timer; set `autoplay=false` to keep automatic progression disabled. | `carousel?.stopAutoplay()` |
845
+
846
+ #### Keyboard controls
847
+
848
+ These keys work while the viewport is focused:
650
849
 
651
- | Method | Description |
850
+ | Key | Action |
652
851
  | --- | --- |
653
- | `next(behavior?)` | Advances by `slidesToScroll`; defaults to smooth scrolling. |
654
- | `previous(behavior?)` | Moves back by `slidesToScroll`. |
655
- | `scrollToIndex(index, options?)` | Makes the index the first visible slide. |
656
- | `startAutoplay()` | Starts or restarts automatic slide progression. |
657
- | `stopAutoplay()` | Stops automatic slide progression. |
852
+ | `ArrowRight`, `PageDown` | Advances by `slidesToScroll`, equivalent to `next()`. |
853
+ | `ArrowLeft`, `PageUp` | Moves back by `slidesToScroll`, equivalent to `previous()`. |
854
+ | `Home` | Moves to the first position. |
855
+ | `End` | Moves to the final valid first-slide position. |
658
856
 
659
- The viewport supports Arrow Left/Right, Page Up/Down, Home, and End. A
660
- long-distance smooth jump becomes immediate so virtualized gaps are not shown.
857
+ A long-distance smooth jump becomes immediate so virtualized gaps are not
858
+ shown.
661
859
 
662
860
  #### Autoplay example
663
861
 
@@ -690,17 +888,134 @@ const products = ref(loadProducts())
690
888
  </template>
691
889
  ```
692
890
 
693
- Autoplay also starts when an initially empty `items` collection receives data.
694
- `stopAutoplay()` pauses the timer and `startAutoplay()` resumes it while the
695
- `autoplay` prop remains enabled. Changes to `autoplayDelay`, `autoplayLoop`, and
696
- `pauseOnHover` take effect at runtime.
891
+ | Action / condition | Result |
892
+ | --- | --- |
893
+ | `autoplay=true` | Starts automatically, including when an initially empty `items` collection receives data. |
894
+ | `stopAutoplay()` | Clears the current timer. Later navigation or pointer events may restart it when `autoplay` remains true. |
895
+ | `startAutoplay()` | Starts or restarts the timer when `autoplay` is true and items are available. |
896
+ | `autoplay=false` | Disables automatic progression until the prop is enabled again. |
897
+ | `autoplayLoop` | At the end, true returns to the start; false stops the timer. |
898
+ | `pauseOnHover` | When true, hovering pauses the timer; leaving resumes it if autoplay is enabled. |
899
+ | Change autoplay settings | `autoplayDelay`, `autoplayLoop`, and `pauseOnHover` take effect at runtime. |
900
+
901
+ ### SkeletonLoader
902
+
903
+ `SkeletonLoader` renders a placeholder with a shimmer animation. Use it on its
904
+ own or inside a list's `loading` slot. It stays visible while mounted; control
905
+ standalone visibility with `v-if`, or let the list's `loading` prop control the
906
+ slot.
907
+
908
+ #### Example
909
+
910
+ Import the library stylesheet once, as shown below or in your application entry
911
+ point. Set `loading` to false when your data request finishes.
912
+
913
+ ```vue
914
+ <script setup lang="ts">
915
+ import { ref } from 'vue'
916
+ import { SkeletonLoader } from 'vue-virtual-flow'
917
+ import 'vue-virtual-flow/style.css'
918
+
919
+ const loading = ref(true)
920
+ </script>
921
+
922
+ <template>
923
+ <SkeletonLoader
924
+ v-if="loading"
925
+ width="100%"
926
+ :height="120"
927
+ aria-label="Loading content…"
928
+ />
929
+ <p v-else>Content is ready.</p>
930
+ </template>
931
+ ```
932
+
933
+ #### Props
934
+
935
+ | Prop | Type | Default | Description |
936
+ | --- | --- | --- | --- |
937
+ | `width` | `number \| string` | `'100%'` | Width in pixels for numbers, or a CSS length such as `'60%'` or `'12rem'`. |
938
+ | `height` | `number \| string` | `16` | Height in pixels for numbers, or a CSS length. |
939
+ | `variant` | `'rectangle' \| 'rounded' \| 'circle'` | `'rounded'` | Placeholder shape. Use equal width and height for a circle. |
940
+ | `animated` | `boolean` | `true` | Enables the shimmer animation. |
941
+ | `ariaLabel` | `string` | `'Loading…'` | Accessible label for the placeholder's `role="status"`. |
942
+
943
+ ```vue
944
+ <SkeletonLoader variant="circle" :width="48" :height="48" />
945
+ <SkeletonLoader variant="rectangle" :height="180" />
946
+ <SkeletonLoader width="60%" :height="20" :animated="false" />
947
+ ```
948
+
949
+ The animation automatically stops when the user prefers reduced motion. When
950
+ composing several decorative skeletons under a shared loading message, pass
951
+ `aria-hidden="true"` to each skeleton to avoid repeated accessible labels.
952
+
953
+ #### Slots, events, and exposed API
954
+
955
+ | API | Support |
956
+ | --- | --- |
957
+ | Slots | None. The component renders a single placeholder. |
958
+ | Events | No custom events. |
959
+ | Exposed methods | None. Control the component with props and `v-if`. |
960
+
961
+ #### Actions
962
+
963
+ | Action | Usage |
964
+ | --- | --- |
965
+ | Show or hide | `<SkeletonLoader v-if="loading" />` |
966
+ | Use a circle | `<SkeletonLoader variant="circle" :width="48" :height="48" />` |
967
+ | Disable shimmer | `<SkeletonLoader :animated="false" />` |
968
+ | Set the accessible label | `aria-label="Loading…"` |
969
+ | Hide a decorative skeleton from screen readers | `aria-hidden="true"` |
970
+
971
+ #### Use in a loading slot
972
+
973
+ Import `SkeletonLoader` alongside `VirtualList`. With your existing `items` and
974
+ `loading` state, replace the loading message with a skeleton:
975
+
976
+ ```vue
977
+ <VirtualList :items="items" :loading="loading" :loading-item-size="64">
978
+ <template #default="{ item }"><FeedCard :item="item" /></template>
979
+ <template #loading>
980
+ <SkeletonLoader :height="40" aria-label="Loading the next page…" />
981
+ </template>
982
+ </VirtualList>
983
+ ```
984
+
985
+ | Setting | Usage |
986
+ | --- | --- |
987
+ | Components with a `loading` slot | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`. |
988
+ | List `loading` prop | Set to true to show the slot; false to hide it. |
989
+ | Skeleton `height` prop | Sets the placeholder height, for example `40` pixels. |
990
+ | List `loadingItemSize` prop | Reserve space for the skeleton plus the row's vertical padding. Example: `40px + 1.5rem = 64px` at a 16px root font size. |
991
+
992
+ #### Customize appearance
993
+
994
+ Set these CSS custom properties on the skeleton or a parent element:
995
+
996
+ | CSS custom property | Default | Description |
997
+ | --- | --- | --- |
998
+ | `--skeleton-background` | `#e2e8f0` | Base background color. |
999
+ | `--skeleton-highlight` | `rgb(255 255 255 / 60%)` | Shimmer highlight color. |
1000
+ | `--skeleton-radius` | `8px` | Corner radius for the `rounded` variant. |
1001
+
1002
+ ```vue
1003
+ <SkeletonLoader
1004
+ :height="80"
1005
+ style="--skeleton-background: #334155; --skeleton-highlight: #475569; --skeleton-radius: 12px"
1006
+ />
1007
+ ```
697
1008
 
698
1009
  ## Shared recipes
699
1010
 
700
1011
  ### Viewport height
701
1012
 
702
- Container-based components accept a number or CSS height string. Numbers are
703
- converted to pixels. `height="fill"` is shorthand for `height="100%"`:
1013
+ | Value / component | Behavior |
1014
+ | --- | --- |
1015
+ | `:height="400"` | Numeric values become pixels. |
1016
+ | `height="60vh"` | CSS height strings are passed through. |
1017
+ | `height="fill"` / `height="100%"` | Uses the parent height. The parent must have an explicit height; development builds warn if this resolves to `0px`. |
1018
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Use the browser window and do not accept a `height` prop. |
704
1019
 
705
1020
  ```vue
706
1021
  <div class="list-container">
@@ -717,15 +1032,14 @@ converted to pixels. `height="fill"` is shorthand for `height="100%"`:
717
1032
  }
718
1033
  ```
719
1034
 
720
- A percentage height only works when its parent has an explicit height.
721
- Development builds warn when it resolves to `0px`.
722
- `WindowDynamicVirtualScroll` uses the browser window and has no `height` prop.
723
-
724
1035
  ### Stable item keys
725
1036
 
726
- For object items, components try `item.id` and then `item.key`. Primitive items
727
- fall back to their index. Provide `itemKey` when the identifier has another
728
- name:
1037
+ | Configuration / data | Key selection |
1038
+ | --- | --- |
1039
+ | `:item-key="(item, index) => …"` | Uses the function result. |
1040
+ | `item-key="uuid"` | Uses a valid `item.uuid`; otherwise tries `id`, `key`, then the index. |
1041
+ | Object without an explicit `itemKey` | Tries `item.id`, then `item.key`, then the index. |
1042
+ | Primitive without a key function | Falls back to the index. |
729
1043
 
730
1044
  ```vue
731
1045
  <VirtualList :items="users" item-key="uuid" />
@@ -742,8 +1056,11 @@ variable-height item.
742
1056
 
743
1057
  ### Load more
744
1058
 
745
- List components use a pixel distance threshold. `ShortMediaFeed` and
746
- `VirtualCarousel` use a remaining-item threshold.
1059
+ | Component | `loadMoreThreshold` | Event |
1060
+ | --- | --- | --- |
1061
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Distance to the end in pixels. | `load-more` |
1062
+ | `ShortMediaFeed` | Number of items after the active item. | `load-more` |
1063
+ | `VirtualCarousel` | Number of slides after the visible group. | `load-more` |
747
1064
 
748
1065
  ```vue
749
1066
  <script setup lang="ts">
@@ -775,14 +1092,20 @@ async function loadMore() {
775
1092
  </template>
776
1093
  ```
777
1094
 
778
- The event fires at most once for the current item count. Appending items allows
779
- the next request. Keep `loading` true for the entire request and set `hasMore`
780
- to false after the final page.
1095
+ | Step / condition | Action / result |
1096
+ | --- | --- |
1097
+ | Receive `load-more` | Start the next request only when `loading` is false and `hasMore` is true. |
1098
+ | Request in progress | Keep `loading=true` for the entire request. |
1099
+ | Receive the next page | Append its items. The event normally fires once per current item count; increasing that count permits another request. |
1100
+ | Request finished | Set `loading=false`; use `finally` when requests may fail. |
1101
+ | Final page | Set `hasMore=false` to stop further requests. |
781
1102
 
782
1103
  ### Pull to refresh
783
1104
 
784
- Pull-to-refresh is available on `VirtualList`, `DynamicVirtualScroll`,
785
- `VirtualScroll`, and `WindowDynamicVirtualScroll`:
1105
+ | Component | Gesture starts at |
1106
+ | --- | --- |
1107
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll` | The top of the component container. |
1108
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | `window.scrollY = 0` |
786
1109
 
787
1110
  ```vue
788
1111
  <VirtualList
@@ -799,21 +1122,51 @@ Pull-to-refresh is available on `VirtualList`, `DynamicVirtualScroll`,
799
1122
  </VirtualList>
800
1123
  ```
801
1124
 
802
- The single-touch gesture begins only at the top. Set `refreshing` to true while
803
- the async refresh runs, then return it to false to close the indicator.
1125
+ | Step / state | Action / result |
1126
+ | --- | --- |
1127
+ | Enable the gesture | `pull-to-refresh` |
1128
+ | Pull down with one finger at the top | Updates the `refresh` slot scope: `pullDistance`, `progress`, and `refreshing`. |
1129
+ | Release after reaching `pullRefreshThreshold` | Emits `refresh` without a payload. |
1130
+ | Handle `refresh` | Set `refreshing=true` while fetching fresh data to keep the indicator open. |
1131
+ | Finish refreshing | Set `refreshing=false` to close the indicator. |
804
1132
 
805
1133
  ## TypeScript exports
806
1134
 
1135
+ | Type | Purpose |
1136
+ | --- | --- |
1137
+ | `ChatVirtualScrollExpose` | Chat ref API and `isAtBottom` state. |
1138
+ | `ChatVirtualScrollProps<T>` | Chat component props. |
1139
+ | `DynamicVirtualScrollProps<T>` | Variable-height list props. |
1140
+ | `ItemKey<T>` | Property name or function used to identify an item. |
1141
+ | `ResponsiveBreakpoint` | Carousel container breakpoint and optional layout overrides. |
1142
+ | `ScrollAlignment` | `'start'`, `'center'`, or `'end'` for list index scrolling. |
1143
+ | `ShortMediaFeedChangeEvent<T>` | Feed `change` payload: `{ index, item }`. |
1144
+ | `ShortMediaFeedExpose` | Feed ref API. |
1145
+ | `ShortMediaFeedProps<T>` | Feed props. |
1146
+ | `SkeletonLoaderProps` | Skeleton size, shape, animation, and accessible label. |
1147
+ | `VirtualCarouselChangeEvent<T>` | Carousel `change` payload: `{ index, item }`. |
1148
+ | `VirtualCarouselExpose` | Carousel navigation and autoplay ref API. |
1149
+ | `VirtualCarouselProps<T>` | Carousel props, including responsive settings. |
1150
+ | `VirtualListExpose` | Alias of `VirtualScrollExpose`. |
1151
+ | `VirtualListProps<T>` | Alias of `DynamicVirtualScrollProps<T>`. |
1152
+ | `VirtualScrollEvent` | Scroll payload: `{ scrollTop, startIndex, endIndex }`; `endIndex` is exclusive. |
1153
+ | `VirtualScrollExpose` | Shared ref API for fixed, dynamic, and window lists/grids. |
1154
+ | `VirtualScrollProps<T>` | Fixed-height list props. |
1155
+ | `WindowDynamicVirtualScrollProps<T>` | Window-based variable-height list props. |
1156
+ | `WindowGirdVirtualScrollProps<T>` | Window-based fixed-row grid props. |
1157
+
807
1158
  ```ts
808
1159
  import type {
809
1160
  ChatVirtualScrollExpose,
810
1161
  ChatVirtualScrollProps,
811
1162
  DynamicVirtualScrollProps,
812
1163
  ItemKey,
1164
+ ResponsiveBreakpoint,
813
1165
  ScrollAlignment,
814
1166
  ShortMediaFeedChangeEvent,
815
1167
  ShortMediaFeedExpose,
816
1168
  ShortMediaFeedProps,
1169
+ SkeletonLoaderProps,
817
1170
  VirtualCarouselChangeEvent,
818
1171
  VirtualCarouselExpose,
819
1172
  VirtualCarouselProps,
@@ -823,11 +1176,28 @@ import type {
823
1176
  VirtualScrollExpose,
824
1177
  VirtualScrollProps,
825
1178
  WindowDynamicVirtualScrollProps,
1179
+ WindowGirdVirtualScrollProps,
826
1180
  } from 'vue-virtual-flow'
827
1181
  ```
828
1182
 
829
1183
  ## Development and release
830
1184
 
1185
+ | Command / lifecycle | Action |
1186
+ | --- | --- |
1187
+ | `bun install` | Install dependencies. |
1188
+ | `bunx playwright install chromium firefox webkit` | Install browsers for smoke tests. |
1189
+ | `bun run dev` | Start the playground. |
1190
+ | `bun run typecheck` | Check TypeScript and Vue types. |
1191
+ | `bun run test` | Run unit tests. |
1192
+ | `bun run test:watch` | Run unit tests in watch mode. |
1193
+ | `bun run build` | Typecheck and build the package with type declarations. |
1194
+ | `bun run typecheck:package` | Check ESM and CommonJS consumer types after building. |
1195
+ | `bun run lint:package` | Validate package exports and type packaging after building. |
1196
+ | `bun run test:browser` | Run Playwright browser tests. |
1197
+ | `bun run check` | Run all checks: unit tests, build, consumer types, package lint, and browser tests. |
1198
+ | `npm pack --dry-run` | Inspect the files that will be published. |
1199
+ | `prepublishOnly` | Automatically runs `bun run check` before publishing. |
1200
+
831
1201
  ```bash
832
1202
  bun install
833
1203
  bunx playwright install chromium firefox webkit