Vertical Dynamic
How to build a feature like this
To virtualize a vertical list whose rows differ in height, uniform-size arithmetic stops being sufficient: each row's offset depends on the heights of all the rows before it. Dynamic mode solves this by measuring each rendered row with a ResizeObserver so exact offsets and the total scroll height emerge from measured sizes rather than a formula. The real cost is that dynamic lists are heavier than uniform ones - they start from an estimate that corrects itself as rows mount, and only mounted rows can be measured.
1. Choose the size strategy that matches your data
item-size accepts four forms. A positive number means uniform heights and O(1) arithmetic - fastest, but only valid when every row really is that tall. An array describes a repeating pattern (e.g. [50, 100]) and a function (item, index) => number a height known up front that varies per row; both let the engine lay out far-off rows from the declared pattern or function without mounting them - avoiding measurement, at the cost of per-item storage rather than a uniform number's O(1). Pass 0, null, or undefined - or omit the prop - to enable dynamic mode, where heights are measured from the DOM. Use dynamic only when heights are content-driven and unknowable until rendered (wrapping text, images, live data); if you can compute them, an array or function skips the measuring cost.
2. Render real rows at a definite, stable height
In dynamic mode the engine measures whatever box each rendered row actually has. Carry the intended size on your data and express it on the row - a min-block-size or a real height - so the box cannot collapse, and reserve space for media (width/height or aspect-ratio) so late content growth does not push neighbors down. The row's measured box (its border-box size, padding included) drives the layout, and a live resize is caught by the same observer and re-lays the list.
The examples also draw the built-in virtual scrollbar (boolean virtual-scrollbar) on the list. Besides consistent cross-browser styling it is a performance improvement: the overlay bar is driven by the engine's own scroll math, so its rendering cost stays flat no matter how long the list grows.
<script setup lang="ts">
import { VirtualScroll } from '@pdanpdan/virtual-scroll';
import '@pdanpdan/virtual-scroll/style.css';
// Each row knows its own height; no `item-size` prop is passed, so heights
// are treated as dynamic and measured from the rendered DOM.
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
height: i % 2 === 0 ? 50 : 100,
}));
</script>
<template>
<VirtualScroll
virtual-scrollbar
class="v-dyn"
:items="items"
:default-item-size="80"
aria-label="Dynamic height list"
>
<template #item="{ item }">
<!-- Omit item-size → ResizeObserver reports this row's block size. -->
<div class="row" :style="{ minBlockSize: `${ item.height }px` }">
#{{ item.id }} - {{ item.height }}px tall
</div>
</template>
</VirtualScroll>
</template>
<style scoped>
.v-dyn {
block-size: 480px; /* fixed-height scroll viewport */
border: 1px solid oklch(50% 0 0 / 0.2);
}
.row {
box-sizing: border-box;
display: flex;
align-items: center;
padding: 0 1rem;
border-bottom: 1px solid oklch(50% 0 0 / 0.1);
}
</style>3. Follow the estimate-then-measure pipeline
Only rows that are mounted can be measured, so any row that has never entered the viewport keeps the fallback default-item-size (default 40). That estimate drives the initial range and the total scroll height until the row renders, at which point the observed height replaces it. Expect the first layout - and any deep scrollToIndex into unmeasured regions - to settle over a frame or two as measurements land. Set default-item-size near your average row height to shrink the initial error; the buffer-before/buffer-after overscan (default 5) keeps rows mounted just off-screen so this measuring happens before they scroll into view.
- Scroll Status
- Directionvertical
- Current Item #-
- Rendered Range #0:0
- DOM Items #—
- Total Size (px)0h
- Viewport Size (px)0h
- Scroll Offset (px)0y
- Controls