@lazy-virtual-scroll/vue

A high-performance virtualized list component for Vue 3 that efficiently renders large datasets with dynamic sizing and lazy loading
2,936
Install
npm install @lazy-virtual-scroll/vue
Latest Version:1.0.1
Vue:3
License:MIT
Last Updated:Aug 27, 2026
Links: GitHub  ·  npm
Maintainer: jvisgotti

Vue Lazy Virtual Scroll

[![npm version](https://img.shields.io/npm/v/@lazy-virt## Advanced Usage with Dynamic Sizing

<template>
  <div style="height: 500px; width: 100%">
    <LazyVirtualScroll
      :totalItems="items.length"
      :itemSize="50"
      :data="items"
      :autoDetectSizes="true"
      :dynamicSizes="expandedItems"
      :scrollDebounce="100"
      direction="column"
      @load="handleLoad"
    >svg)](https://www.npmjs.com/package/@lazy-virtual-scroll/vue)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

A high-performance virtualized list component for Vue 3 that efficiently renders large datasets with dynamic sizing, lazy loading, and bi-directional scrolling support.

## Features

- **Virtualized Rendering**: Only renders items currently visible in the viewport
- **Dynamic Sizing**: Automatically detects and handles items of varying heights
- **Lazy Loading**: Load data on-demand as the user scrolls
- **Bi-directional Scrolling**: Support for both vertical and horizontal scrolling
- **Performance Optimized**: Debounced and throttled scroll handling
- **Flexible Data Structure**: Support for continuous or fragmented datasets
- **TypeScript Support**: Full type definitions included

## Installation

```bash
# npm
npm install @lazy-virtual-scroll/vue

# yarn
yarn add @lazy-virtual-scroll/vue

# pnpm
pnpm add @lazy-virtual-scroll/vue

Basic Usage

<template>
  <div style="height: 500px; width: 100%">
    <LazyVirtualScroll
      :totalItems="items.length"
      :itemSize="50"
      :data="items"
      @load="handleLoad"
      @hide="handleHide"
    >
      <template #default="{ item, index }">
        <div class="item">
          {{ item.text }}
        </div>
      </template>
      <template #loading="{ index }">
        <div class="item loading">
          Loading item {{ index }}...
        </div>
      </template>
    </LazyVirtualScroll>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LazyVirtualScroll from 'vue-lazy-virtual-scroll';

const items = ref(Array.from({ length: 10000 }, (_, i) => ({ 
  id: i, 
  text: `Item ${i}` 
})));

## Basic Usage

```vue
<template>
  <div style="height: 500px; width: 100%">
    <LazyVirtualScroll
      :totalItems="items.length"
      :itemSize="50"
      :data="items"
      @load="handleLoad"
      @hide="handleHide"
    >
      <template #default="{ item, index }">
        <div class="item">
          {{ item ? item.text : 'Loading...' }}
        </div>
      </template>
      <template #loading="{ index }">
        <div class="item loading">
          Loading item {{ index }}...
        </div>
      </template>
    </LazyVirtualScroll>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LazyVirtualScroll from '@lazy-virtual-scroll/vue';

const items = ref(Array.from({ length: 10000 }, (_, i) => ({ 
  id: i, 
  text: `Item ${i}` 
})));

const handleLoad = ({ startIndex, endIndex }) => {
  console.log(`Loading items from ${startIndex} to ${endIndex}`);
};

const handleHide = ({ startIndex, endIndex }) => {
  console.log(`Hiding items from ${startIndex} to ${endIndex}`);
};
</script>

<style>
.item {
  height: 50px;
  padding: 10px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box; /* Ensures padding is included in height */
}
.item.loading {
  background-color: #f5f5f5;
  color: #999;
}
</style>

Slots

Default Slot (#default)

The default slot is used to render each item in the list:

<LazyVirtualScroll :totalItems="1000" :itemSize="60">
  <template #default="{ item, index }">
    <div class="list-item">
      <h3>Item {{ index }}</h3>
      <p>{{ item.content }}</p>
    </div>
  </template>
</LazyVirtualScroll>

Slot Props:

  • item (any): The data item from your data array or datasets. Will be undefined if data hasn't been loaded yet.
  • index (number): The index of the item in the list

Loading Slot (#loading)

The loading slot is used to render items that are still loading:

<LazyVirtualScroll :totalItems="1000" :itemSize="60">
  <template #default="{ item, index }">
    <!-- Regular item content -->
    <div class="list-item">{{ item.content }}</div>
  </template>
  
  <template #loading="{ index }">
    <div class="loading-item">
      <div class="spinner"></div>
      <span>Loading item {{ index }}...</span>
    </div>
  </template>
</LazyVirtualScroll>

Slot Props:

  • index (number): The index of the loading item

If the #loading slot is not provided, the #default slot will be used with item as undefined.

Events

@load Event

Emitted when new items become visible and need to be loaded:

<template>
  <LazyVirtualScroll @load="handleLoad" />
</template>

<script setup>
const handleLoad = ({ startIndex, endIndex }) => {
  console.log(`Need to load items from ${startIndex} to ${endIndex}`);
  
  // Example: Fetch data for this range
  fetchData(startIndex, endIndex).then(newData => {
    // Update your reactive data
    newData.forEach((item, i) => {
      items.value[startIndex + i] = item;
    });
  });
};
</script>

Payload:

  • startIndex (number): First index that needs to be loaded
  • endIndex (number): Last index that needs to be loaded

@hide Event

Emitted when items go out of view:

<template>
  <LazyVirtualScroll @hide="handleHide" />
</template>

<script setup>
const handleHide = ({ startIndex, endIndex }) => {
  console.log(`Items ${startIndex} to ${endIndex} are now hidden`);
  
  // Example: Clean up resources or mark items for garbage collection
  cleanupItems(startIndex, endIndex);
};
</script>

Payload:

  • startIndex (number): First index that is now hidden
  • endIndex (number): Last index that is now hidden

@scroll Event

Emitted when the user scrolls:

<template>
  <LazyVirtualScroll @scroll="handleScroll" />
</template>

<script setup>
const handleScroll = (scrollPosition) => {
  console.log(`Current scroll position: ${scrollPosition}px`);
  
  // Example: Update URL or save scroll position
  updateScrollPosition(scrollPosition);
};
</script>

Payload:

  • scrollPosition (number): Current scroll position in pixels

Advanced Example

<template>
  <div style="height: 500px; width: 100%">
    <LazyVirtualScroll
      :totalItems="items.length"
      :itemSize="50"
      :data="items"
      :autoDetectSizes="true"
      :dynamicSizes="expandedItems"
      :scrollDebounce="100"
      direction="column"
      @load="handleLoad"
      @hide="handleHide"
    >
      <template #default="{ item, index }">
        <div 
          class="item" 
          :class="{'expanded': index in expandedItems}"
        >
          <div class="item-header" @click="toggleExpand(index)">
            {{ item.text }}
            <span v-if="index in expandedItems">▲</span>
            <span v-else>▼</span>
          </div>
          
          <div 
            v-if="index in expandedItems"
            class="item-content"
            :style="{
              height: `${expandedItems[index]}px`,
              minHeight: `${expandedItems[index]}px`
            }"
          >
            <p>Expanded content for item {{ index }}</p>
          </div>
        </div>
      </template>
      <template #loading="{ index }">
        <div class="item loading">
          Loading item {{ index }}...
        </div>
      </template>
    </LazyVirtualScroll>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LazyVirtualScroll from 'vue-lazy-virtual-scroll';

const items = ref(Array.from({ length: 10000 }, (_, i) => ({ 
  id: i, 
  text: `Item ${i}` 
})));

const expandedItems = ref({});

const toggleExpand = (index) => {
  if (index in expandedItems.value) {
    delete expandedItems.value[index];
  } else {
    expandedItems.value[index] = 300; // expanded height
  }
  expandedItems.value = { ...expandedItems.value };
};

const handleLoad = ({ startIndex, endIndex }) => {
  console.log(`Visible range: ${startIndex} - ${endIndex}`);
};

const handleHide = ({ startIndex, endIndex }) => {
  console.log(`Hidden range: ${startIndex} - ${endIndex}`);
};
</script>

<style>
.item {
  padding: 10px;
  min-height: 50px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box; /* Ensures padding is included in height */
}
.item.expanded {
  background-color: #f0f8ff;
}
.item-header {
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}
.item-content {
  padding: 10px;
  background-color: #f9f9f9;
}
.item.loading {
  background-color: #f5f5f5;
  color: #999;
}
</style>

Props

Prop Type Default Description
totalItems number (required) Total number of items in the list
itemSize number (required) Base height/width of each item in pixels
data any[] [] Array of data items to render
datasets Dataset[] [] Alternative to data for fragmented datasets
direction 'row' | 'column' 'column' Scroll direction
itemBuffer number 3 Number of items to render outside visible area
scrollThrottle number 0 Throttle scroll events (milliseconds)
scrollDebounce number 0 Debounce scroll events (milliseconds)
scrollStart number 0 Initial scroll position
dynamicSizes { [itemIndex: string]: number } {} Manual size overrides for specific items
autoDetectSizes boolean false Automatically detect item sizes
minItemSize number 0 Minimum size for dynamically sized items
sortDatasets boolean true Automatically sort datasets by startingIndex
outerMaxLengthCssValue string '100%' Maximum length CSS value for the outer container
outerMinLengthCssValue string '100%' Minimum length CSS value for the outer container
outerLengthCssValue string '100%' Length CSS value for the outer container
listItemStyle { [key: string]: string } {} Custom styles for list items

Events

Event Payload Description
@load { startIndex: number; endIndex: number } Emitted when new items become visible and need to be loaded
@hide { startIndex: number; endIndex: number } Emitted when items go out of view and are hidden
@scroll number Emitted on scroll with current scroll position

Slots

Name Props Description
default { item: any, index: number } Template for rendering each item
loading { index: number } Template for rendering loading state

Event Examples

Using @load and @hide for Data Management

<template>
  <div style="height: 500px; width: 100%">
    <LazyVirtualScroll
      :totalItems="100000"
      :itemSize="60"
      @load="handleLoad"
      @hide="handleHide"
      @scroll="handleScroll"
    >
      <template #default="{ item, index }">
        <div class="item">
          Item {{ index }} {{ item ? `- ${item.text}` : '(Loading...)' }}
        </div>
      </template>
      <template #loading="{ index }">
        <div class="item loading">
          Loading item {{ index }}...
        </div>
      </template>
    </LazyVirtualScroll>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LazyVirtualScroll from '@lazy-virtual-scroll/vue';

const loadedRanges = ref(new Set());
const hiddenRanges = ref(new Set());

const handleLoad = ({ startIndex, endIndex }) => {
  console.log(`Loading items ${startIndex} to ${endIndex}`);
  
  // Track loaded ranges
  const rangeKey = `${startIndex}-${endIndex}`;
  loadedRanges.value.add(rangeKey);
  
  // Simulate async data loading
  setTimeout(() => {
    console.log(`Loaded items ${startIndex} to ${endIndex}`);
  }, 100);
};

const handleHide = ({ startIndex, endIndex }) => {
  console.log(`Hiding items ${startIndex} to ${endIndex}`);
  
  // Track hidden ranges for cleanup
  const rangeKey = `${startIndex}-${endIndex}`;
  hiddenRanges.value.add(rangeKey);
  
  // Optional: Clean up data that's no longer visible
  // This can help with memory management for large datasets
};

const handleScroll = (scrollPosition) => {
  console.log(`Scrolled to position: ${scrollPosition}`);
};
</script>

<style>
.item {
  height: 60px;
  padding: 10px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box; /* Ensures padding is included in height */
}
.item.loading {
  opacity: 0.6;
}
</style>

Working with Fragmented Datasets

For scenarios where your data is loaded in chunks or comes from different sources, you can use the datasets prop instead of data:

const datasets = ref([
  { startingIndex: 0, data: [{id: 0, text: 'Item 0'}, {id: 1, text: 'Item 1'}, /* ... */] },
  { startingIndex: 100, data: [{id: 100, text: 'Item 100'}, /* ... */] },
  // More dataset chunks...
]);
<LazyVirtualScroll
  :datasets="datasets"
  :totalItems="10000"
  :itemSize="50"
  <!-- ...other props -->
>
  <!-- slots -->
</LazyVirtualScroll>

Dynamic Sizing

The component supports dynamic item sizes in two ways:

  1. Manual Size Specification:

    const dynamicSizes = ref({
      5: 100,  // Item at index 5 has height 100px
      10: 200, // Item at index 10 has height 200px
    });
    
    <LazyVirtualScroll
      :dynamicSizes="dynamicSizes"
      <!-- ...other props -->
    >
      <!-- slots -->
    </LazyVirtualScroll>
    
  2. Automatic Size Detection:

    <LazyVirtualScroll
      :autoDetectSizes="true"
      <!-- ...other props -->
    >
      <!-- slots -->
    </LazyVirtualScroll>
    

Performance Optimization

For optimal performance with large lists:

  1. Use both scrollThrottle and scrollDebounce to limit scroll event processing:

    <LazyVirtualScroll
      :scrollThrottle="16"  <!-- ~60fps -->
      :scrollDebounce="100" <!-- Final update after scrolling stops -->
      <!-- ...other props -->
    >
      <!-- slots -->
    </LazyVirtualScroll>
    
  2. Keep component renders lightweight by using v-memo for list items:

    <template #default="{ item, index }">
      <div v-memo="[item.id, item.text]" class="item">
        {{ item.text }}
      </div>
    </template>
    

Dynamic Sizing

The component supports dynamic item sizes in two ways:

  1. Manual Size Specification:

    <script setup>
    const dynamicSizes = ref({
      5: 100,  // Item at index 5 has height 100px
      10: 200, // Item at index 10 has height 200px
    });
    </script>
    
    <template>
      <LazyVirtualScroll
        :dynamicSizes="dynamicSizes"
        <!-- ...other props -->
      />
    </template>
    
  2. Automatic Size Detection:

    <LazyVirtualScroll
      :autoDetectSizes="true"
      <!-- ...other props -->
    />
    

Performance Optimization

For optimal performance with large lists:

  1. Use both scrollThrottle and scrollDebounce to limit scroll event processing:

    <LazyVirtualScroll
      :scrollThrottle="16"
      :scrollDebounce="100"
      <!-- ...other props -->
    />
    
  2. Use v-memo for complex items to prevent unnecessary re-renders:

    <LazyVirtualScroll>
      <template #default="{ item, index }">
        <div v-memo="[item?.id, item?.updatedAt]" class="complex-item">
          <!-- Complex item content -->
          {{ item?.content }}
        </div>
      </template>
    </LazyVirtualScroll>
    
  3. Keep item templates simple and avoid heavy computations in templates:

    <!-- Good: Simple, reactive data -->
    <template #default="{ item, index }">
      <div class="item">
        <h3>{{ item.title }}</h3>
        <p>{{ item.description }}</p>
      </div>
    </template>
    
    <!-- Avoid: Heavy computations in templates -->
    <template #default="{ item, index }">
      <div class="item">
        <!-- This will run on every render -->
        <h3>{{ processComplexTitle(item.rawData) }}</h3>
      </div>
    </template>
    

Running unit tests

Run nx test @lazy-virtual-scroll/vue to execute the unit tests via Vitest.

License

MIT

Related Packages

@pdanpdan/virtual-scroll

Virtual scrolling for Vue 3: only the rows near the viewport are in the DOM.

8,720 13
vue-virtual-scroller-kit

Virtual list, table, grid and tree for Vue 3 — dynamic row heights, grouping, st...

3,195 0
vue-virtual-flow

Typed, accessible virtual lists, feeds and carousels for Vue 3.

1,557 0
@ceriousdevtech/vue-cerious-scroll

Vue 3 bindings for CeriousScroll — virtualized lists, native tables, and Masonry...

2,670 20
@wxwzl/vue-virtual-list

A high-performance virtual list for Vue 3 (spacer-flow layout, zero transforms,...

449 4

Version History

Version Released Vue License
1.0.1 ^3.0.0 MIT
1.0.0 ^3.0.0 MIT