Server-Mode VDataTable in Vuetify 3: Pushing Pagination to the Backend
Large tables kill client performance. Vuetify 3 VDataTable server mode moves pagination, sorting and filtering to the backend via update:options with explicit loading and total count handling.
17 Dec 2025, 04:47 UTC

Opening a table with 200k rows in Vuetify 3 and letting the client paginate it is a fast way to lock the UI. The useful takeaway is to keep the table dumb and move pagination, sorting and filtering to the server with VDataTable server mode, using server-items-length, loading and the update:options event. This assumes Vuetify 3.4+ and Vue 3 Composition API. Vuetify 2 APIs are not interchangeable.
Why server mode matters for VDataTable
VDataTable in Vuetify 3 is built for Vue 3 and exposes reactive composables like useTheme and useDisplay. For tables, the key decision is who owns the data slice.
In client mode the component receives a full items array and does paging locally. That is fine for hundreds of rows. With tens of thousands, memory, initial fetch time and sort/filter responsiveness degrade.
Server mode flips the contract. The component only renders the current page and emits option changes. Your app fetches the correct slice and tells the table how many total rows exist. The table never assumes it has the whole dataset.
Wiring update:options to a fetch
VDataTable emits update:options whenever page, items-per-page, sort or filter changes. The payload contains page, itemsPerPage, sortBy and search. You are responsible for fetching and setting loading.
Missing server-items-length or leaving loading false during a fetch causes pagination controls to appear active while data is stale, which is the most common UI glitch in server mode.
Minimal server-mode shape
<template>
<v-data-table
:headers="headers"
:items="items"
:items-per-page="options.itemsPerPage"
:server-items-length="total"
:loading="loading"
@update:options="onUpdate"
></v-data-table>
</template>
import { ref } from 'vue'
const headers = ref([
{ title: 'Id', key: 'id' },
{ title: 'Name', key: 'name' }
])
const items = ref([])
const total = ref(0)
const loading = ref(false)
const options = ref({ page: 1, itemsPerPage: 25, sortBy: [] })
async function onUpdate(newOptions) {
options.value = newOptions
loading.value = true
try {
const params = {
page: options.value.page,
pageSize: options.value.itemsPerPage,
sortBy: options.value.sortBy[0]?.key,
sortDesc: options.value.sortBy[0]?.order === 'desc'
}
const data = await fetchFromBackend(params)
items.value = data.items
total.value = data.total
} finally {
loading.value = false
}
}
Run this in a Vue 3 app with Vuetify 3 installed. Required permissions are normal network access to the backend endpoint. The placeholder fetchFromBackend should return an object with items and total. Do not assume a specific response shape; map it to the table.
Custom rendering stays possible. Scoped slots for headers, items and footers let you keep table logic generic while customizing presentation without duplicating fetch logic.
Trade-off and limitation
Server mode pushes complexity to you. You must debounce rapid option changes, handle errors without leaving loading true, and keep server-items-length in sync with the backend count. If the backend count drifts, pagination shows empty pages.
Theming and responsiveness are unrelated but worth noting: Vuetify 3 uses CSS variables generated from a theme object, enabling runtime light/dark switching without rebuilds in 3.4+. That behavior is version sensitive; older releases used SASS variables.
Verification without inventing output: create a minimal app, inspect the component API in your IDE types to confirm server-items-length, items-per-page and update:options exist for your installed version. Log the emitted options and toggle loading to confirm the overlay disables interaction during fetches. Check package.json for the Vuetify version and compare prop names against local type definitions.
Do not use Vuetify Labs components for this pattern. Labs components have less stable APIs and can change between minor releases, which is risky for data fetching contracts.
Actionable next step
Pick one table that is slow today, add :server-items-length and :loading, and route @update:options to an existing backend list endpoint. Keep the first implementation read-only, verify total count matches, then add sort and filter mapping. This keeps the UI responsive while the data layer scales.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.