Core Plugins

Virtual

Render long lists efficiently with windowed scrolling.


Overview

When a list grows past a few hundred rows, rendering every row up front becomes slow with initial paint stalls, scrolling stutters, and climbing memory. The Virtual plugin solves this by only mounting the rows currently visible in the scroll viewport, plus a small overscan buffer for smooth scrolling. A list of ten thousand items can keep just twenty or so rows in the DOM at any time while the user scrolls through smoothly.

Each row is measured as it renders, so taller rows (multi-line text, embedded media, expanded details) coexist with shorter ones without configuration. There's no fixed row-height requirement.

Virtualization is a rendering optimization, not a data-fetching one. The full data array stays in memory, and only its DOM representation is windowed. For server-side pagination see databases.


Setup

Virtual is included in manifest.js with all core plugins, or can be selectively loaded.

<script src="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.js"></script>

Basic Usage

Wrap an x-for template in a scrolling container marked with x-virtual. The container needs a bounded height like a pixel value, percentage, viewport unit, or flex layout in order to support a scroll viewport.


<div x-data="{ team: /* 500 entries */ }">
    <div x-virtual style="height: 300px; overflow: auto">
        <template x-for="member in team" :key="member.id">
            <div>
                <p x-text="member.name"></p>
                <small x-text="member.role"></small>
            </div>
        </template>
    </div>
</div>

Five hundred rows are in the array, but only the visible window is rendered as you scroll. The plugin sets overflow: auto and position: relative on the container automatically if they aren't already set.

A single <template> child is supported per x-virtual container, and the template's x-for and :key are consumed by the plugin (Alpine doesn't double-render them).


Customization

Pass options as an object expression on the directive.

Property Type Default Description
estimate Number 50 Initial per-row height in pixels, used for unmeasured rows. A closer estimate produces less scroll-position drift on first paint.
overscan Number 3 Rows to render above and below the visible window. Higher values smooth out fast scrolling at the cost of more DOM.
<div x-virtual="{ estimate: 80, overscan: 5 }" style="height: 600px; overflow: auto">
    <template x-for="item in $x.products" :key="item.id">
        <div>...</div>
    </template>
</div>

Without these options the defaults will work for most uniform lists. Tune estimate higher when rows are tall (cards, images), and raise overscan if you see brief blank flashes during fast wheel-scrolling.


With Data Sources

The plugin subscribes to the source expression through Alpine, so any reactive change to a local or cloud data source (adds, deletes, sorts, filters) automatically updates the rendered window.

This makes virtualization a natural pair with $search and $query. Combine a search input with $x.<source>.$search() and the virtual list re-renders against the filtered results without rebuilding the whole DOM:


<div x-data="{ term: '' }">
    <input type="text" placeholder="Filter products..." aria-label="Filter products" x-model="term">
    <div x-virtual style="height: 600px; overflow: auto">
        <template x-for="product in $x.products.$search(term, 'name')" :key="product.id">
            <div>
                <p x-text="product.name"></p>
                <small x-text="'$' + product.price"></small>
            </div>
        </template>
    </div>
</div>

Mixed-Height Rows

Rows with variable content like multi-line descriptions, embedded images, or expanded details will work without any extra configuration. Each row is measured on first render and the scroll offsets adjust to its actual height. There is no fixed-height requirement.


<div x-virtual style="height: 600px; overflow: auto">
    <template x-for="log in $x.logs" :key="log.id">
        <div>
            <span x-text="log.time"></span>
            <span x-text="log.level"></span>
            <span x-text="log.message"></span>
        </div>
    </template>
</div>

In this example, short and tall log lines coexist correctly. The scroll position stays accurate, the scrollbar reflects real total height, and rows snap to their measured offsets the first time each one becomes visible.