Horizontal Dynamic
How this example is built
To virtualize a horizontal list whose items differ in width, the engine cannot place items arithmetically the way it does with uniform sizes - each item's offset depends on the widths of everything before it. Dynamic mode solves this by measuring each rendered cell with a ResizeObserver so totals follow the measured sizes. Layout starts from an estimate that is corrected as cells mount, and totals become exact once the relevant range has been measured.
1. Choose the size strategy that matches your data
item-size accepts four forms, each trading speed against flexibility. A positive number means uniform sizes and pure O(1) arithmetic, and is valid only when every item is that size. An array describes a repeating width pattern (e.g. [150, 300]), and a function (item, index) => number expresses a width known up front that varies per item; both let the engine lay out far-off items from the declared pattern or function without mounting them - avoiding dynamic 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 entirely - to switch to dynamic mode, where sizes are measured from the DOM. Use dynamic only when widths are content-driven and cannot be known before render (wrapping text, media, responsive cells); if you can compute them, an array or function skips the measuring cost.
2. Give every cell a definite, stable width
A measured cell must render at the width you intend and not change after it mounts. Drive the width from your data and apply it as an explicit inline size on the cell (the snippet uses inlineSize from a per-item width), and reserve space for late media content so a post-mount resize does not shift neighbors. Because each rendered box is observed, a live resize (window resize, data-driven width change) is caught and the axis is re-laid automatically.
<script setup lang="ts">
import { VirtualScroll } from '@pdanpdan/virtual-scroll';
import '@pdanpdan/virtual-scroll/style.css';
// Each item knows its own width; no `item-size` prop is passed, so the engine
// treats the axis as dynamic and measures the rendered boxes.
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
width: i % 2 === 0 ? 150 : 300,
}));
</script>
<template>
<VirtualScroll
virtual-scrollbar
class="h-dyn"
direction="horizontal"
:items="items"
:default-item-size="220"
aria-label="Dynamic width list"
>
<template #item="{ item }">
<!-- Omit item-size → ResizeObserver reports this box's inline size. -->
<div class="card" :style="{ inlineSize: `${ item.width }px` }">
#{{ item.id }} - {{ item.width }}px
</div>
</template>
</VirtualScroll>
</template>
<style scoped>
.h-dyn {
block-size: 160px; /* rows span the viewport height */
border: 1px solid oklch(50% 0 0 / 0.2);
}
.card {
box-sizing: border-box;
block-size: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0 1rem;
border-inline-end: 1px solid oklch(50% 0 0 / 0.1);
}
</style>3. Accept the estimate-then-measure pipeline
Only mounted cells can be measured, so the width of an item that has never been in the viewport is unknown. Until a row mounts it keeps the fallback default-item-size (default 40), which drives the initial range and the total scroll width; as cells enter the window their real measurements replace the estimate, which is why a dynamic list "settles": the first paint (and any deep scrollToIndex) can be slightly off and correct itself over a couple of frames. Set default-item-size near your average width to shrink the initial error.
- Scroll Status
- Directionhorizontal
- Current Item #-
- Rendered Range #0:0
- DOM Items #—
- Total Size (px)0w
- Viewport Size (px)0w
- Scroll Offset (px)0x
- Controls