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 best 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 scrolling a very long dataset stays smooth. That speed is the reward for a promise you make to the engine - that every item is exactly the declared width - so this mode is the right choice only while that promise 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, remember 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. 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';
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 genuinely 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