VirtualGrid
A virtual grid that arranges items in rows and columns. Column count can be fixed or auto-calculated from columnWidth and the container width.
Props
items
T[]
Data array.
columns
number · default: 0
Fixed column count. Pass 0 to auto-compute from columnWidth.
columnWidth
number · default: 200
Cell width for auto-column calculation.
rowHeight
number · default: 200
Cell height in px — the initial estimate when dynamicRowHeight is on.
gap
number · default: 8
Gap between cells in px.
keyField
string · default: 'id'
Key field.
overscan
number · default: 2
Extra rows outside the viewport.
isLoading
boolean · default: false
Shows skeleton when items is empty.
motionBlur
boolean · default: false
Apply a CSS blur that scales with scroll velocity while scrolling fast.
dynamicRowHeight
boolean · default: false
Measure each row's actual height via ResizeObserver instead of a fixed rowHeight (row height = max of that row's cells).
containerRole
string · default: 'grid'
ARIA role for the outer container. Set to 'none' (together with rowRole and itemRole) when embedding the grid inside another semantic widget that already owns the "grid" role, so this element is removed from the accessibility tree instead of nesting one role inside another.
rowRole
string · default: 'row'
ARIA role for each row wrapper.
itemRole
string · default: 'gridcell'
ARIA role for each cell.
aria-rowcount/aria-colcount(on the container) andaria-rowindex/aria-colindex(on each row/cell) are only emitted while the corresponding role stays in the grid/row/cell family. OverridecontainerRole/rowRole/itemRoleaway from that family (e.g. to'none') and the matching index/count attributes stop being emitted too, since they'd be invalid ARIA without it.
Slots
default
Scope: { item: T, index: number, row: number, col: number }
Cell content.
empty
Scope: No scope
Shown when items is empty.
skeleton
Scope: No scope
Shown when empty and isLoading.
Emits
scroll
Payload: Event
visible-range-change
Payload: { start: number; end: number }
Exposed API
scrollTo(index, options?)
Scroll to a cell index. Unlike the other components' scrollTo, VirtualGrid derives the target row from index and doesn't take an align parameter.
getScrollElement()
Returns the element that actually scrolls — pair with VirtualScrollbar.
gridRef.value?.scrollTo(index)
gridRef.value?.scrollTo(index, { behavior: 'smooth' })
gridRef.value?.getScrollElement() // pair with VirtualScrollbarExample
<script setup lang="ts">
import { VirtualGrid } from 'vue-virtual-scroller-kit'
interface Photo {
id: number
url: string
title: string
}
const photos: Photo[] = Array.from({ length: 10_000 }, (_, i) => ({
id: i,
url: `https://picsum.photos/seed/${i}/200/200`,
title: `Photo ${i + 1}`,
}))
</script>
<template>
<VirtualGrid
:items="photos"
:column-width="220"
:row-height="220"
:gap="12"
style="height: 600px"
>
<template #default="{ item }">
<div class="photo-card">
<img :src="item.url" :alt="item.title" />
<p>{{ item.title }}</p>
</div>
</template>
</VirtualGrid>
</template>Dynamic row height — when cell content height varies (e.g. captions of different lengths), row height is measured per row instead of fixed:
<VirtualGrid
:items="photos"
:column-width="220"
:row-height="220"
dynamic-row-height
:gap="12"
style="height: 600px"
>
<template #default="{ item }">
<div class="photo-card" style="height: auto">
<img :src="item.url" :alt="item.title" />
<p>{{ item.title }}</p>
<p v-if="item.caption" class="photo-card__caption">{{ item.caption }}</p>
</div>
</template>
</VirtualGrid>With
dynamicRowHeight, cells no longer get a fixedheightfrom the grid — give themheight: auto(or leaveheightunset) so their content determines the row's real height.