Skip to content

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) and aria-rowindex/aria-colindex (on each row/cell) are only emitted while the corresponding role stays in the grid/row/cell family. Override containerRole/rowRole/itemRole away 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.

ts
gridRef.value?.scrollTo(index)
gridRef.value?.scrollTo(index, { behavior: 'smooth' })
gridRef.value?.getScrollElement() // pair with VirtualScrollbar

Example ​

vue
<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:

vue
<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 fixed height from the grid — give them height: auto (or leave height unset) so their content determines the row's real height.