Keyboard shortcut: Ctrl + K
Get started

Virtual list

VVirtualList renders only the rows near its viewport, for lists too long to render whole. It is its own scroll container: give it a height, or a bounded height in your own CSS.

Usage

vue
Invoice 10.00 €
Invoice 279.19 €
Invoice 3158.38 €
Invoice 4237.57 €
Invoice 5316.76 €
Invoice 6395.95 €
Invoice 7475.14 €
Invoice 8554.33 €
Invoice 9633.52 €
Invoice 10712.71 €
<script setup lang="ts">
import { VVirtualList } from 'vectis-ui'

const invoices = Array.from({ length: 10000 }, (_, i) => ({
  id: i + 1,
  amount: ((i * 7919) % 100000) / 100,
}))
</script>

<template>
  <VVirtualList :items="invoices" item-key="id" :height="320" label="Invoices" class="list">
    <template #default="{ item }">
      <div class="row">
        <span>Invoice {{ item.id }}</span>
        <span>{{ item.amount.toFixed(2) }} €</span>
      </div>
    </template>
  </VVirtualList>
</template>

<style scoped>
.list {
  max-inline-size: 420px;
  border: 1px solid var(--vectis-color-border);
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-block-size: 40px;
  padding-inline: var(--vectis-space-3);
  border-block-end: 1px solid var(--vectis-color-border);
}
</style>

Examples

Rows of different heights

itemSize is only the height assumed for rows not rendered yet. Each row is measured once and remembered under its itemKey; set it when rows can be inserted, removed or reordered.

vue
Note 1

Paid in three instalments after a dispute about the delivery date, settled with a discount on the next order.

Note 2

Paid on delivery.

Note 3

Paid on delivery.

Note 4

Paid in three instalments after a dispute about the delivery date, settled with a discount on the next order.

Note 5

Paid on delivery.

Note 6

Paid on delivery.

Note 7

Paid in three instalments after a dispute about the delivery date, settled with a discount on the next order.

Note 8

Paid on delivery.

Note 9

Paid on delivery.

Note 10

Paid in three instalments after a dispute about the delivery date, settled with a discount on the next order.

<script setup lang="ts">
import { VVirtualList } from 'vectis-ui'

const notes = Array.from({ length: 2000 }, (_, i) => ({
  id: i + 1,
  text:
    i % 3 === 0
      ? 'Paid in three instalments after a dispute about the delivery date, settled with a discount on the next order.'
      : 'Paid on delivery.',
}))
</script>

<template>
  <VVirtualList
    :items="notes"
    item-key="id"
    :item-size="64"
    :height="320"
    label="Notes"
    class="list"
  >
    <template #default="{ item }">
      <article class="note">
        <strong>Note {{ item.id }}</strong>
        <p>{{ item.text }}</p>
      </article>
    </template>
  </VVirtualList>
</template>

<style scoped>
.list {
  max-inline-size: 360px;
  border: 1px solid var(--vectis-color-border);
}

.note {
  padding: var(--vectis-space-2) var(--vectis-space-3);
  border-block-end: 1px solid var(--vectis-color-border);
}

.note p {
  margin: var(--vectis-space-1) 0 0;
  color: var(--vectis-color-text-muted);
}
</style>

Scrolling to a row

The scrollToIndex(index, align) method renders the row if needed and brings it into view: at the start, center or end, or with the smallest movement by default.

vue
Invoice 1
Invoice 2
Invoice 3
Invoice 4
Invoice 5
Invoice 6
Invoice 7
Invoice 8
Invoice 9
Invoice 10
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VVirtualList, type VirtualListAlign } from 'vectis-ui'

const invoices = Array.from({ length: 10000 }, (_, i) => ({ id: i + 1 }))
const list = ref<{ scrollToIndex: (index: number, align?: VirtualListAlign) => void } | null>(null)
</script>

<template>
  <div class="stack">
    <div class="actions">
      <VButton @click="list?.scrollToIndex(4999, 'center')">Go to invoice 5,000</VButton>
      <VButton variant="outline" @click="list?.scrollToIndex(0, 'start')">Back to the top</VButton>
    </div>
    <VVirtualList
      ref="list"
      :items="invoices"
      item-key="id"
      :height="320"
      label="Invoices"
      class="list"
    >
      <template #default="{ item }">
        <div class="row">Invoice {{ item.id }}</div>
      </template>
    </VVirtualList>
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 420px;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-2);
}

.list {
  border: 1px solid var(--vectis-color-border);
}

.row {
  display: flex;
  align-items: center;
  min-block-size: 40px;
  padding-inline: var(--vectis-space-3);
  border-block-end: 1px solid var(--vectis-color-border);
}
</style>

Infinite scroll

hasMore emits load-more as the end of the list comes into view. Append the next rows to items; loading ends the list with a loading row meanwhile.

vue
Event 1
Event 2
Event 3
Event 4
Event 5
Event 6
Event 7
Event 8
Event 9
Event 10
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VVirtualList } from 'vectis-ui'

const total = 200

function page(from: number) {
  return Array.from({ length: 40 }, (_, i) => ({ id: from + i + 1 }))
}

const events = ref(page(0))
const loading = ref(false)
const hasMore = computed(() => events.value.length < total)

// Stands in for a request to your API.
function loadMore() {
  loading.value = true
  setTimeout(() => {
    events.value = [...events.value, ...page(events.value.length)]
    loading.value = false
  }, 600)
}
</script>

<template>
  <VVirtualList
    :items="events"
    item-key="id"
    :height="320"
    :loading="loading"
    :has-more="hasMore"
    label="Activity"
    class="list"
    @load-more="loadMore"
  >
    <template #default="{ item }">
      <div class="row">Event {{ item.id }}</div>
    </template>
  </VVirtualList>
</template>

<style scoped>
.list {
  max-inline-size: 420px;
  border: 1px solid var(--vectis-color-border);
}

.row {
  display: flex;
  align-items: center;
  min-block-size: 40px;
  padding-inline: var(--vectis-space-3);
  border-block-end: 1px solid var(--vectis-color-border);
}
</style>

Focusable rows

The list has the list role and takes the focus, so the keyboard scrolls it. The row holding the focus stays rendered while the list scrolls away. When every row holds a control, tabindex="-1" removes the list’s own tab stop. Find-in-page only reaches rendered rows: offer a search for long lists.

vue
Invoice 1
Invoice 2
Invoice 3
Invoice 4
Invoice 5
Invoice 6
Invoice 7
Invoice 8
Invoice 9
Invoice 10
<script setup lang="ts">
import { VButton, VVirtualList } from 'vectis-ui'

const invoices = Array.from({ length: 10000 }, (_, i) => ({ id: i + 1 }))
</script>

<template>
  <VVirtualList
    :items="invoices"
    item-key="id"
    :height="320"
    label="Invoices"
    tabindex="-1"
    class="list"
  >
    <template #default="{ item }">
      <div class="row">
        <span>Invoice {{ item.id }}</span>
        <VButton size="sm" variant="ghost" :aria-label="`Open invoice ${item.id}`">Open</VButton>
      </div>
    </template>
  </VVirtualList>
</template>

<style scoped>
.list {
  max-inline-size: 420px;
  border: 1px solid var(--vectis-color-border);
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-block-size: 40px;
  padding-inline: var(--vectis-space-3);
  border-block-end: 1px solid var(--vectis-color-border);
}
</style>

API

Props

PropTypeDefault
itemsT[]none
virtual-list.api.VVirtualList.props.items
itemKeystringnone
virtual-list.api.VVirtualList.props.itemKey
itemSizenumber40
virtual-list.api.VVirtualList.props.itemSize
overscannumber5
virtual-list.api.VVirtualList.props.overscan
initialCountnumber10
virtual-list.api.VVirtualList.props.initialCount
heightnumber | stringnone
virtual-list.api.VVirtualList.props.height
labelstringnone
virtual-list.api.VVirtualList.props.label
loadingbooleanfalse
virtual-list.api.VVirtualList.props.loading
loadingTextstringnone
virtual-list.api.VVirtualList.props.loadingText
hasMorebooleanfalse
virtual-list.api.VVirtualList.props.hasMore

Events

EventType
load-more[]
virtual-list.api.VVirtualList.events.loadMore

Slots

SlotType
defaultVirtualListItemSlotProps<T>
virtual-list.api.VVirtualList.slots.default
loading{}
virtual-list.api.VVirtualList.slots.loading

Types

Types used in the API tables. Import exported types from vectis-ui.

export interface VirtualListItemSlotProps<T> {
  item: T
  index: number
}