useDynamicScroller (Headless Dynamic Items)
useDynamicScroller is the headless composable for virtual lists whose item size must be measured after render.
Use it when you need custom markup, but item size still has to be measured from the DOM after render.
When to use it
- You need semantic markup such as table rows, list items, or design-system wrappers.
- Item height or width is not known ahead of time.
- You still want pooled rendering and DOM reuse instead of rendering every visible item from scratch.
- You need dynamic measurement without relying on bundled wrapper markup.
Mental model
useDynamicScrollercombines two concerns:useRecycleScrollerfor pooled rendering, scroll math, and virtualization statevDynamicScrollerItemfor per-item size measurement
- You render from
pool. - Each
viewinsidepoolexposes the original item directly atview.item. - Advanced measured metadata still lives on
view.itemWithSize. totalSizestill belongs on your inner wrapper.startSpacerSizeandendSpacerSizeexpose the spacer sizes needed byflowMode.getViewStyle(view)exposes pooled positioning styles for custom integrations. Generic elements use transforms by default,disableTransformswitches them totop/left, andflowModekeeps active views in native flow.- When you bind
v-dynamic-scroller-item="{ view, ... }", the directive:- derives
item,active, andindexfrom the pooled view - measures the DOM element for unknown-size updates
- applies recycled-view positioning and visibility styles automatically
- uses
toppositioning for table rows and transforms for generic elements
- derives
TypeScript generics
Pass the item type as the generic parameter when you want typed headless helpers and strict item inputs:
const dynamicScroller = useDynamicScroller<Message>({
items: messages.value,
keyField: 'id',
direction: 'vertical',
minItemSize: 48,
el: scrollerEl,
})
dynamicScroller.pool.value[0]?.item.text
dynamicScroller.pool.value[0]?.itemWithSize.id
dynamicScroller.getItemSize(messages.value[0])The same declared type also flows into useDynamicScrollerItem<TItem>() when you use the lower-level measurement helper directly.
Like the other scroller APIs, keyField can be either a string field name or a resolver function with the signature (item, index) => string | number.
Directive contract
Use the directive with the pooled view:
v-dynamic-scroller-item="{
view,
}"Supported binding fields:
view: required in the recommended headless pathwatchData: deep-watch fallback, usually not recommendedemitResize: emit resize callbacks when measurement changesonResize: optional callback for resize notifications
In the recommended view-based path, you do not need to pass item, active, index, or per-item positioning styles manually.
Return values you will use most
pool: render-ready pooled views. This is the main render source.visiblePool: active views in visible index order. Useful for readouts, debugging, or derived UI state.totalSize: virtual size for the inner wrapper.startSpacerSize: spacer size before the active pooled window inflowMode.endSpacerSize: spacer size after the active pooled window inflowMode.scrollToItem(index, options?): jump to a logical item index withalign,smooth, andoffset.scrollToPosition(px, options?): scroll to an absolute pixel offset.findItemIndex(offset): resolve a pixel offset back to an item index.getItemOffset(index): read the known starting offset for an item.getItemSize(item, index?): read the measured size for an item.getViewStyle(view): build pooled wrapper positioning styles for the current direction.cacheSnapshot: current serializable size snapshot.restoreCache(snapshot): restore previously known sizes when the item sequence matches.forceUpdate(clear?): trigger a recalculation, optionally clearing known sizes.
Render checklist
- Give the outer scroller a fixed size and overflow behavior.
- Add an inner wrapper with
position: relativeandminHeight/minWidthfromtotalSize. - Render every entry in
pool. - Bind the pooled
viewintov-dynamic-scroller-item.
If you enable flowMode, render start and end spacer elements from startSpacerSize and endSpacerSize instead of applying totalSize to an absolutely positioned inner wrapper. This is the path used by the headless table demo.
When that headless path renders a semantic table, pair it with useTableColumnWidths so column widths stay locked while pooled rows churn.
Common pitfalls
- Forgetting
minItemSizehurts the initial layout and scroll math. - Rendering from
visiblePoolinstead ofpoolreduces the effectiveness of DOM reuse. view.itemWithSizeis still available, but ordinary rendering should useview.item.watchDataonly exists for legacy no-ResizeObserverfallbacks and is heavier than the default path.- If you prepend into chat-style data, enable
shiftin the composable options so the viewport stays anchored. - Set
disableTransformwhen generic pooled wrappers must avoid translate transforms. flowModeonly supports vertical single-axis layouts in v1. It is meant for native block or table flow, not grids or horizontal virtualization.
Full example
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'
import { useDynamicScroller } from 'vue-virtual-scroller'
const rows = ref([
{ id: 1, title: 'Alpha', body: 'Unknown-size content' },
{ id: 2, title: 'Beta', body: 'This row can wrap and grow' },
])
const scrollerEl = useTemplateRef<HTMLElement>('scrollerEl')
const {
pool,
totalSize,
vDynamicScrollerItem,
} = useDynamicScroller(computed(() => ({
items: rows.value,
keyField: 'id',
direction: 'vertical' as const,
minItemSize: 48,
el: scrollerEl.value,
})))
</script>
<template>
<div
ref="scrollerEl"
class="scroller"
>
<div :style="{ minHeight: `${totalSize}px`, position: 'relative' }">
<article
v-for="view in pool"
:key="view.id"
v-dynamic-scroller-item="{ view }"
>
<h4>{{ view.item.title }}</h4>
<p>{{ view.item.body }}</p>
</article>
</div>
</div>
</template>Related guides
useRecycleScrollerfor fixed-size or pre-sized headless listsuseTableColumnWidthsfor locking semantic table columns after measurementuseWindowScrollerfor headless window-based virtualization- Headless table demo for a semantic table example