Skip to content

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

  • useDynamicScroller combines two concerns:
    • useRecycleScroller for pooled rendering, scroll math, and virtualization state
    • vDynamicScrollerItem for per-item size measurement
  • You render from pool.
  • Each view inside pool exposes the original item directly at view.item.
  • Advanced measured metadata still lives on view.itemWithSize.
  • totalSize still belongs on your inner wrapper.
  • startSpacerSize and endSpacerSize expose the spacer sizes needed by flowMode.
  • getViewStyle(view) exposes pooled positioning styles for custom integrations. Generic elements use transforms by default, disableTransform switches them to top/left, and flowMode keeps active views in native flow.
  • When you bind v-dynamic-scroller-item="{ view, ... }", the directive:
    • derives item, active, and index from the pooled view
    • measures the DOM element for unknown-size updates
    • applies recycled-view positioning and visibility styles automatically
    • uses top positioning for table rows and transforms for generic elements

TypeScript generics

Pass the item type as the generic parameter when you want typed headless helpers and strict item inputs:

ts
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:

vue
v-dynamic-scroller-item="{
  view,
}"

Supported binding fields:

  • view: required in the recommended headless path
  • watchData: deep-watch fallback, usually not recommended
  • emitResize: emit resize callbacks when measurement changes
  • onResize: 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 in flowMode.
  • endSpacerSize: spacer size after the active pooled window in flowMode.
  • scrollToItem(index, options?): jump to a logical item index with align, smooth, and offset.
  • 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: relative and minHeight/minWidth from totalSize.
  • Render every entry in pool.
  • Bind the pooled view into v-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 minItemSize hurts the initial layout and scroll math.
  • Rendering from visiblePool instead of pool reduces the effectiveness of DOM reuse.
  • view.itemWithSize is still available, but ordinary rendering should use view.item.
  • watchData only exists for legacy no-ResizeObserver fallbacks and is heavier than the default path.
  • If you prepend into chat-style data, enable shift in the composable options so the viewport stays anchored.
  • Set disableTransform when generic pooled wrappers must avoid translate transforms.
  • flowMode only supports vertical single-axis layouts in v1. It is meant for native block or table flow, not grids or horizontal virtualization.

Full example

vue
<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>

Released under the MIT License.