A native JS Adapter and a Vue 3 Hook for variable-height single-column or multi-column / masonry virtual lists. Ensures the DOM stays fully consistent with Adapter state, and supports both window scrolling and custom containers.
x-virtual provides:
- VirtualListAdapter — a native JS single-column virtual list adapter responsible for virtual range calculation, scroll listening, and ResizeObserver. It does NOT handle rendering.
- useVirtualLayout — a Vue 3 Composition Hook for multi-column / masonry layouts, built on top of the Adapter.
Core principle: Adapter and rendering are separated. All getter methods must be used inside the onChange callback.
Core library (native JS usage)
npm install @x-virtual/coreVue 3 Hook (multi-column / masonry wrapper)
npm install @x-virtual/vue<div id="container" style="width:300px; height:400px; position:relative; overflow:auto;">
<div id="spacer"></div>
</div>
<script type="module">
import { VirtualListAdapter } from '@x-virtual/core'
const data = Array.from({ length: 1000 }, (_, i) => ({ id: i, text: 'Item ' + i }))
const container = document.getElementById('container')
const spacer = document.getElementById('spacer')
const adapter = new VirtualListAdapter({
getContainerElement: () => container,
itemSelector: '.v-item',
getKey: i => data[i]?.id,
estimatedHeight: (i) => 10,
overscan: 10,
gap: 0,
//isWindowScroll: false, // whether to use page/window scrolling
onChange: ({ hasNewData }) => {
const items = adapter.getRangeItems()
container.innerHTML = items.map(item => `
<div class='v-item' style='${Object.entries(item.style).map(([k,v])=>k+":"+v).join(";")}' >
${item.key}: ${data[item.index]?.text}
</div>
`).join('')
spacer.style.height = adapter.getTotalHeight() + 'px'
if (hasNewData) requestAnimationFrame(() => adapter.updateRenderedItemSize())
}
})
Object.assign(container.style, VirtualListAdapter.containerStyle)
requestAnimationFrame(() => {
// adapter.reset() //dispose
adapter.init()
adapter.isInitialized && adapter.setItemCount(data.length)
})
</script><template>
<div class="test-scroll" :style="containerStyle" ref="container">
<!-- placeholder element -->
<div :style="{ height: `${totalHeight}px` }"></div>
<!-- rendered item -->
<div
v-for="item in sliceData"
:key="item.key"
class="test-content-item"
:style="item.style"
:data-columnIndex="item.columnIndex"
>
<img
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
style="width: 47px; height: 47px; border-radius: 50%"
/>
<p>{{ dataSource[item.index]?.el + '-----' + dataSource[item.index]?.text }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useVirtualLayout } from '@x-virtual/vue'
const dataSource = ref(Array.from({ length: 1000 }, (_, i) => ({ el: 'El'+i, text: 'Text'+i })))
const container = ref(null)
const { sliceData, totalHeight, scrollTo, containerStyle, calculateContainerPageTop } =
useVirtualLayout(dataSource, {
columnCount: 1,
gap: 8,
itemSelector: '.test-content-item',
getContainerElement: () => container.value,
getKey: i => dataSource.value[i]?.el,
estimatedHeight: (i) => 60,
})
</script>
<style>
.test-scroll {
position: relative;
overflow: auto;
}
.test-content-item {
position: absolute;
}
</style>| Prop / Method | Type | Description |
|---|---|---|
| getContainerElement | Function | Returns the scrolling container DOM |
| itemSelector | String | Selector for rendered items |
| getKey | Function | Unique and stable key |
| estimatedHeight | Function | Estimated item height |
| overscan | Number | Number of buffered items before/after visible range |
| gap | Number | Spacing between items |
| onChange | Function | Adapter update callback (must call getRangeItems() and getTotalHeight() inside this callback and sync containerStyle) |
| isWindowScroll | Boolean | Whether to use window scrolling; affects scrollTop calculation |
| calculateContainerPageTop | Function | Updates listTop when using window scroll or when container has a non-zero top offset; call after mounted or resize |
| updateRenderedItemSize | Function | Call after DOM rendering so ResizeObserver attaches correctly |
| getRangeItems | Function | Gets current visible virtual items, including .top and .style
|
| getTotalHeight | Function | Gets total virtual list height |
| scrollToIndex | Function | Scrolls to a specific global index |
| setItemCount | Function | Updates total item count |
| containerStyle | Object | Adapter returns container style that must be manually synced to DOM |
- The placeholder element must be placed before items and must independently occupy the total height.
- All data access methods must be called inside
onChange; otherwise the virtual range and DOM will become inconsistent. -
updateRenderedItemSize()must be called after DOM insertion. -
calculateContainerPageTop()must be called when using window scrolling or when the container has a non-zero top offset. - Item DOM must use
position: absolute, and the container must useposition: relative. -
getKeymust be unique and stable. - The Adapter is framework-agnostic, but native JS usage requires direct DOM manipulation and manual synchronization of
containerStyle.
MIT