useWindowScroller (Headless Window Scrolling)
useWindowScroller is the headless composable for virtual lists that follow browser window scrolling.
Use it when the browser viewport should drive virtualization, but you still need full control over markup, wrappers, and styling.
When to use it
- The page itself scrolls, not an inner fixed-height container.
- You need custom markup in normal page flow.
- You still want pooled rendering, scroll helpers,
shift, and cache restore on the window-scrolling path.
Mental model
useWindowScrollerisuseRecycleScrollerwithpageModeforced on.- Your root
elstays in normal page flow. It should not become the scrolling element. poolis still the render source when you want the usual DOM reuse behavior.totalSizestill belongs on an inner wrapper so the virtual extent matches the whole list.- You can provide optional
beforeandafterrefs when leading or trailing page content should be included in the virtual offset math.
TypeScript generics
useWindowScroller accepts the same generic item parameter as useRecycleScroller, so the returned pool and helper methods stay item-aware:
const windowScroller = useWindowScroller<Row>({
items: rows.value,
keyField: 'id',
direction: 'vertical',
itemSize: 44,
minItemSize: null,
sizeField: 'size',
typeField: 'type',
buffer: 200,
prerender: 0,
emitUpdate: false,
updateInterval: 0,
}, rootEl)
windowScroller.pool.value[0]?.item.labelFor object items, keyField follows the same rules as useRecycleScroller: use a string property name for compile-time field validation, or pass a resolver function with the signature (item, index) => string | number for derived keys. itemSize also supports a resolver function (item, index) => number for variable-size lists. Variable-size sizeField keeps the same compile-time checks as useRecycleScroller when itemSize is null.
Required inputs
useWindowScroller(options, el, before?, after?)
options: same core options asuseRecycleScroller, exceptpageModeis always treated astrueel: ref for the root scroller element in page flowbefore: optional ref for content rendered before the virtual list inside the same rootafter: optional ref for content rendered after the virtual list inside the same root
Common options:
itemskeyFielddirectionitemSizeminItemSizesizeFieldtypeFieldbuffershiftcachedisableTransformprerenderemitUpdateupdateInterval
gridItems still requires a numeric fixed itemSize. Function-based sizes are not supported in grid mode.
Return values you will use most
pool: render-ready pooled viewsvisiblePool: active views in visible index ordertotalSize: virtual size for the inner wrapperscrollToItem(index, options?): jump to a logical item index withalign,smooth, andoffsetscrollToPosition(px, options?): scroll the page to an absolute list offsetgetScroll(): current viewport range intersected with this listfindItemIndex(offset): resolve a pixel offset back to an item indexgetItemOffset(index): read the starting pixel offset for an itemgetItemSize(index): read the known size for an itemgetViewStyle(view): build the pooled wrapper positioning styles used by the component pathcacheSnapshot: current serializable size snapshotrestoreCache(snapshot): restore a previous snapshot when the item sequence matchesupdateVisibleItems(itemsChanged, checkPositionDiff?): force recalculation
Render checklist
- Keep the outer root in page flow.
- Add an inner wrapper with
position: relativeandminHeightorminWidthfromtotalSize. - Render every entry in
pool. - Apply
getViewStyle(view)to each pooled wrapper. - Hide inactive views instead of filtering them out.
Common pitfalls
- Do not give the root element a fixed scrolling height if the page is supposed to own scrolling.
pageModeis already built in here. Do not try to layer separate page-mode logic on top.- Render from
pool, notvisiblePool, when you want the usual recycling behavior. - If item sizes must be measured from the DOM after render, use
useDynamicScrollerinstead. beforeandafterrefs matter when surrounding content inside the root changes the list's effective offset.
Full example
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'
import { useWindowScroller } from 'vue-virtual-scroller'
interface Row {
id: number
label: string
}
const rows = ref<Row[]>(
Array.from({ length: 2000 }, (_, index) => ({
id: index + 1,
label: `Row ${index + 1}`,
})),
)
const rootEl = useTemplateRef<HTMLElement>('rootEl')
const beforeEl = useTemplateRef<HTMLElement>('beforeEl')
const afterEl = useTemplateRef<HTMLElement>('afterEl')
const {
pool,
totalSize,
scrollToItem,
getViewStyle,
} = useWindowScroller(computed(() => ({
items: rows.value,
keyField: 'id',
direction: 'vertical' as const,
itemSize: 44,
minItemSize: null,
sizeField: 'size',
typeField: 'type',
buffer: 200,
shift: false,
prerender: 0,
emitUpdate: false,
updateInterval: 0,
})), rootEl, beforeEl, afterEl)
</script>
<template>
<section ref="rootEl" class="window-list">
<header ref="beforeEl" class="window-list__intro">
<button @click="scrollToItem(500, { align: 'start', smooth: true })">
Jump to row 501
</button>
</header>
<div class="window-list__inner" :style="{ minHeight: `${totalSize}px` }">
<div
v-for="view in pool"
:key="view.nr.id"
class="window-list__row"
:style="getViewStyle(view)"
>
{{ (view.item as Row).label }}
</div>
</div>
<footer ref="afterEl" class="window-list__outro">
End of virtualized content
</footer>
</section>
</template>
<style scoped>
.window-list__inner {
position: relative;
}
.window-list__row {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 44px;
display: flex;
align-items: center;
padding: 0 12px;
box-sizing: border-box;
border-bottom: 1px solid #eee;
}
</style>Related guides
useRecycleScrollerfor headless lists inside their own scroll containeruseDynamicScrollerfor unknown-size headless items