---
title: "Pagination & Load More"
description: "Client-side auto pagination, server-side manual pagination with loading state, and bottom-trigger load-more for infinite scroll."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/data-table/pagination"
---
# Pagination & Load More

> Client-side auto pagination, server-side manual pagination with loading state, and bottom-trigger load-more for infinite scroll.

## Client-Side Auto Pagination

Passing `v-model:pagination` triggers automatic client-side pagination; the component slices the data locally and renders the default pagination bar:

```vue [DataTableClientPaginationExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

## Server-Side Manual Pagination

Set `paginationOptions.manualPagination` to `true` and provide `rowCount` to hand off pagination to the caller — pass only the current page's data via `data`. The example below uses local slicing to simulate a server; in a real scenario you should request the backend whenever `pagination` changes:

```vue [DataTableServerPaginationExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

// 用本地切片模拟服务端：仅持有当前页数据 + 已知总条数 rowCount
const all = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const rowCount = all.length
const pageData = computed(() => {
  const start = pagination.value.pageIndex * pagination.value.pageSize
  return all.slice(start, start + pagination.value.pageSize)
})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="pageData"
    :columns="columns"
    :pagination-options="{ manualPagination: true, rowCount }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

> [!TIP]
> 
> Combine 
> 
> :loading
> 
> , 
> 
> :sorting-options="{ manualSorting: true }"
> 
>  and 
> 
> v-model:sorting
> 
>  for server-side sorting; reset 
> 
> pagination.pageIndex
> 
>  to 0 when a search is triggered.

## Page-Count Mode

When the backend returns only the total page count rather than total rows, use `paginationOptions.pageCount` to specify the total pages directly:

```vue [DataTablePageCountPaginationExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

// 服务端只返回页数（pageCount），不返回总条数
const all = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const pageCount = computed(() => Math.ceil(all.length / pagination.value.pageSize))
const pageData = computed(() => {
  const start = pagination.value.pageIndex * pagination.value.pageSize
  return all.slice(start, start + pagination.value.pageSize)
})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="pageData"
    :columns="columns"
    :pagination-options="{ manualPagination: true, pageCount }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

## Page Size Switching

When `paginationUi.pageSizes` has more than one item, a page-size switcher is shown:

```vue [DataTablePageSizesPaginationExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{ pageSizes: [5, 10, 20, 50] }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

## Pagination Summary and Labels

`v-model:row-selection` combined with `row-key` syncs the selected count; `paginationUi`'s `showSelectedCount`, `showRowRange` and `text` control the summary area's display and labels:

```vue [DataTablePaginationSummaryExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
const selection = ref<RowSelectionState>({})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    v-model:row-selection="selection"
    :data="data"
    :columns="columns"
    row-key="id"
    select-on-row-click
    :pagination-ui="{
      showSelectedCount: true,
      showRowRange: true,
      text: { total: '总计', item: '人', range: '当前', selected: '勾选' }
    }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

## Passing Props to the Pagination Component

`paginationUi.paginationProps` passes through `UPagination` props such as `showEdges` and `siblingCount`; `pageSizeSelectProps` passes through page-size selector props:

```vue [DataTablePaginationPassthroughExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{
      pageSizes: [5, 10, 20],
      paginationProps: { showEdges: true, siblingCount: 1, color: 'success' },
      pageSizeSelectProps: { size: 'sm', variant: 'subtle' }
    }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
```

## Pagination Slots

`#pagination-summary` and `#pagination-actions` replace the summary area and actions area respectively:

```vue [DataTablePaginationSlotsExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{ show: true }"
    :ui="{ root: 'max-h-[60vh]' }"
  >
    <template #pagination-summary="{ page, pageCount, rowCount }">
      <span class="text-sm text-muted">
        第 {{ page }} / {{ pageCount }} 页，共 {{ rowCount }} 条
      </span>
    </template>
    <template #pagination-actions="{ page, pageCount, setPage }">
      <UButton size="xs" variant="ghost" icon="i-lucide-chevron-left" :disabled="page <= 1" @click="setPage(page - 1)" />
      <UButton size="xs" variant="ghost" icon="i-lucide-chevron-right" :disabled="page >= pageCount" @click="setPage(page + 1)" />
    </template>
  </MDataTable>
</template>
```

The `#pagination` slot receives the full pagination state (`page`, `pageCount`, `from`, `to`, `setPage`, etc.) and replaces the entire default pagination bar:

```vue [DataTablePaginationFullExample.vue]
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :ui="{ root: 'max-h-[60vh]' }"
  >
    <template #pagination="{ page, pageCount, from, to, setPage }">
      <div class="flex items-center justify-between rounded-md border border-default px-3 py-2">
        <span class="text-sm text-muted">显示 {{ from }}-{{ to }}</span>
        <div class="flex items-center gap-2">
          <UButton size="xs" variant="soft" :disabled="page <= 1" @click="setPage(page - 1)">
            上一页
          </UButton>
          <span class="text-sm tabular-nums">{{ page }} / {{ pageCount }}</span>
          <UButton size="xs" variant="soft" :disabled="page >= pageCount" @click="setPage(page + 1)">
            下一页
          </UButton>
        </div>
      </div>
    </template>
  </MDataTable>
</template>
```

## `loading` Loading State

`loading` is passed through to the underlying `UTable` to display a loading indicator, suitable for initial data fetch or page-turn waits:

```vue [DataTableLoadingExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号', size: 100 },
  { accessorKey: 'name', header: '姓名', size: 120 },
  { accessorKey: 'role', header: '岗位', size: 160 }
]
</script>

<template>
  <MDataTable :columns="columns" :data="[]" loading />
</template>
```

## `loadMore` Bottom-Triggered Load

Passing a `load-more` callback enables infinite scroll mode (the built-in pagination bar is automatically hidden). When the callback returns a Promise, the component automatically derives `loading` during the load:

```vue [DataTableLoadMoreExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 15
const TOTAL = 60
const items = ref<Person[]>([])
const loaded = ref(0)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]

// 模拟异步分页：每次追加一页，返回 Promise 让组件自动派生 loading
async function loadMore() {
  await new Promise(r => setTimeout(r, 600))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset() {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :load-more-distance="100"
      :ui="{ root: 'h-[60vh]' }"
    />
  </div>
</template>
```

> [!NOTE]
> 
> A scroll container with a constrained height (e.g. 
> 
> ui.root: 'h-[60vh]'
> 
> ) is required to produce scrolling and trigger the bottom-load callback.

## `canLoadMore` Stop Loading

When `can-load-more` is `false`, triggering stops — you can force a pause independently of remaining data (e.g. no more data, or stop after an error):

```vue [DataTableLoadMoreCanLoadMoreExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 48
const items = ref<Person[]>([])
const loaded = ref(0)
const paused = ref(false)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

async function loadMore() {
  await new Promise(r => setTimeout(r, 500))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => !paused.value && loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <USwitch v-model="paused" label="暂停加载" />
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>
```

## `loadMoreDistance` Trigger Threshold

`load-more-distance` sets the distance-from-bottom threshold in pixels (default 100) — a larger value triggers loading earlier:

```vue [DataTableLoadMoreDistanceExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 60
const items = ref<Person[]>([])
const loaded = ref(0)
const distance = ref(100)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

async function loadMore() {
  await new Promise(r => setTimeout(r, 500))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <USelect
        v-model="distance"
        :items="[
          { label: 'distance 40（贴底触发）', value: 40 },
          { label: 'distance 100（默认）', value: 100 },
          { label: 'distance 240（提前触发）', value: 240 }
        ]"
        value-key="value"
        size="xs"
        class="w-52"
      />
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :load-more-distance="distance"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>
```

## Caller-Managed Loading

If you want the caller to manage loading state, make `load-more` return synchronously (without handing back a Promise) and pass `:loading` manually, in contrast to the auto-derived form:

```vue [DataTableLoadMoreManualLoadingExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 48
const items = ref<Person[]>([])
const loaded = ref(0)
const loading = ref(false)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

// 同步返回（不交回 Promise），由调用方自行接管 loading
function loadMore(): void {
  if (loading.value) return
  loading.value = true
  setTimeout(() => {
    const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
    items.value = [...items.value, ...next]
    loaded.value += next.length
    loading.value = false
  }, 600)
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
  loading.value = false
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <UBadge :color="loading ? 'primary' : 'neutral'" variant="subtle" size="sm">
        {{ loading ? 'loading…' : 'idle' }}
      </UBadge>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :loading="loading"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>
```


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
