Horizontal Fixed
How to build a feature like this
To virtualize a horizontal strip in which every item has the same width, switch the axis to horizontal and give the engine a numeric item-size. Uniform sizes are the cheapest case for virtualization: the visible window, each item's position, and the total scroll width are all derived arithmetically (index × itemSize), so range math stays arithmetic and long datasets scroll without extra work. That speed depends on every item being exactly the declared width, so this mode fits only while that holds.
1. Put the list on the horizontal axis and size it
direction defaults to 'vertical'; pass direction="horizontal" to scroll along the inline (width) axis. The component renders its own scrollable host, which needs a definite block size so rows have a height to span (rows stretch to the viewport height automatically) and a constrained inline size so content overflows sideways rather than wrapping. In a flex/grid parent, add min-height: 0 / min-width: 0 so the box is allowed to shrink below its content. The buffer-before / buffer-after props (default 5) keep a few extra items mounted past each edge so fast panning does not flash blank cells; they count items, so widen them if cells are wide or travel is quick.
2. Pass a real array and render each row from item
The mainstream data model is an ordinary array of item objects. The #item slot receives both item and index; render your cell from item and use index for position or keys. Only the window in view (plus the buffer) is mounted, but the full array still lives in memory - virtualization saves DOM nodes, not your data.
The examples also draw the built-in virtual scrollbar (boolean virtual-scrollbar) on the list. 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';
const items = Array.from({ length: 10_000 }, (_, i) => ({
id: i,
label: `Item ${ i }`,
}));
</script>
<template>
<VirtualScroll
virtual-scrollbar
class="h-list"
direction="horizontal"
:items="items"
:item-size="100"
aria-label="Horizontal list"
>
<template #item="{ item, index }">
<div class="h-row">
<strong>{{ item.label }}</strong>
<span>#{{ index }}</span>
</div>
</template>
</VirtualScroll>
</template>
<style scoped>
/* A definite block size fixes the row height; rows stretch to it and the
content scrolls sideways. The inline size comes from the layout. */
.h-list {
block-size: 140px;
border: 1px solid oklch(50% 0 0 / 0.2);
}
/* Each cell is item-size (100px) wide along the scroll axis. The row root must
fill it so nothing overlaps or leaks. */
.h-row {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
inline-size: 100%;
block-size: 100%;
border-inline-end: 1px solid oklch(50% 0 0 / 0.1);
}
</style>3. Use an index-only array when rows carry no data
When a cell is fully described by its position (a tick, an ordinal sequence, a pagination page) you can skip allocating data objects entirely. items only needs to provide a length - entries are read only for the rendered window - so a sparse new Array(count) works: the item slot prop is undefined for every hole and you render from index. Paired with a numeric item-size this keeps memory flat even into the millions of rows. Do not reach for it when rows carry content or vary in size; those need real data plus an item-size function/array or dynamic measurement.
<script setup lang="ts">
import { VirtualScroll } from '@pdanpdan/virtual-scroll';
// A row fully described by its index needs no data: only the array length is
// used and every `item` slot prop is undefined, so render from `index`.
const items = new Array(1_000_000);
</script>
<template>
<VirtualScroll
virtual-scrollbar
class="h-list"
direction="horizontal"
:items="items"
:item-size="100"
aria-label="Index-only horizontal list"
>
<template #item="{ index }">
<div class="h-row">#{{ index }}</div>
</template>
</VirtualScroll>
</template>4. Keep item-size equal to the rendered width
item-size is a contract, not a hint: the engine uses it for the total scroll width, the range math, and any scrollbar geometry. Each rendered row is mounted into a cell exactly that wide and as tall as the viewport, so the row root should fill the box (inline-size: 100%; block-size: 100%, borders included via box-sizing: border-box). Content wider than the declared size clips or overlaps; narrower content leaves gaps. When widths vary, move off uniform mode to an array, a function, or dynamic measurement (see the dynamic-width example) rather than fighting a fixed number.
- Scroll Status
- Directionhorizontal
- Current Item #-
- Rendered Range #0:0
- DOM Items #—
- Total Size (px)0w
- Viewport Size (px)0w
- Scroll Offset (px)0x
- Controls