# Pagination

> Client pagination, page size, disabling pagination, the pager accessible names and the pointer to server pagination.

Page: https://svelte-lean.vercel.app/table/pagination · Markdown: https://svelte-lean.vercel.app/table/pagination.md · Index: https://svelte-lean.vercel.app/llms.txt

A page is a slice of the filtered, sorted row model. pageIndex and pageSize live in the state, the pager is a set of native buttons and a select, and a page size of zero shows every row.

- **Source**: core.ts (paginateNodes)data-table.svelte
- **Tests**: core.test.tsdata-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

Client pagination slices after search, filters and sorting, so the page count follows the filtered total. The page index is clamped: a filter that empties later pages moves the user to the last page that exists instead of showing nothing. Rows pinned through `rowPinning` stay at the top or bottom of every page and are not counted against the page size.

The pager announces its status line through a polite live region and names every button from the messages (`first`, `previous`, `next`, `last`, `pageNumber`). Between the arrows it shows numbered pages: the first, the last and the current page with its neighbours, with a gap icon where pages are left out; the current page carries `aria-current="page"`. Below a 600px viewport the numbers and the page-size select are hidden and the arrows remain. The page-size select offers `pageSizeOptions` (default 10, 20, 50 and 100), the current size and, for a size of zero, the `allRows` option. Because paging is state, controls outside the table can drive it, and an application that pages on the server switches to manual mode with the same state object.

## Client pagination

Client or server pagination with controlled page index and page size.

Pagination.svelte

```svelte
<script lang="ts">
	import { DataTable, defaultState, type TableState } from '@svelte-lean/table';
	import { memberColumns, pick } from '$lib/demo/columns';
	import { people } from '$lib/demo/data';

	const data = people(36);
	const columns = pick(memberColumns(), ['name', 'team', 'region', 'revenue']);
	let tableState = $state<Partial<TableState>>(defaultState({ pageSize: 5 }));
	const pages = $derived(Math.max(1, Math.ceil(data.length / (tableState.pageSize || data.length))));
</script>

<!-- A pager outside the table: Button primitives writing pageIndex into the same state. -->
<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" disabled={!tableState.pageIndex}
	onclick={() => (tableState = { ...tableState, pageIndex: (tableState.pageIndex ?? 0) - 1 })}>Previous</button>
<span>Page {(tableState.pageIndex ?? 0) + 1} of {pages}</span>
<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" disabled={(tableState.pageIndex ?? 0) >= pages - 1}
	onclick={() => (tableState = { ...tableState, pageIndex: (tableState.pageIndex ?? 0) + 1 })}>Next</button>

<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	bind:state={tableState}
	pageSizeOptions={[5, 10, 25]}
	label="Members"
/>
```

The pager under the table is built in, with numbered pages and pageSizeOptions of 5, 10 and 25; the Previous and Next buttons above it are the Button primitive writing pageIndex into the same bound state, so the two stay in step. Change the page size to see the page count follow.

- Sorting, searching or filtering resets `pageIndex` to zero.
- The status line reads the filtered total, not the length of `data`.

## Page-size menu

The page-size control is a native `<select>` by default, and the platform draws its list. With `pageSizeSelect="custom"` it is the [Select primitive](https://svelte-lean.vercel.app/primitives/select.md) instead: a trigger button with the value and a listbox popup drawn from the design tokens in every engine, opening above the pager when there is no room below. The native `<select>` stays inside it and keeps the value, so the pager still works without scripts.

PageSizeMenu.svelte

```svelte
<script lang="ts">
	// The Select primitive: its behavior and its styles. The table's markup lives in a package,
	// where @svelte-lean/vite does not look for markers, so the behavior is registered here.
	import '@svelte-lean/primitives/select/register';
	import '@svelte-lean/styles/select.css';
	import { DataTable } from '@svelte-lean/table';
	import '@svelte-lean/table/style.css';
</script>

<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	pageSizeOptions={[5, 10, 25, 50]}
	pageSizeSelect="custom"
	label="Members by page size"
/>
```

Open the Rows per page control under the table: a listbox in the top layer with the current size checked. Arrow keys move, Enter chooses, Escape closes, and focus stays on the trigger (the APG select-only combobox).

- The behavior is `@svelte-lean/primitives/select/register` (Tier 2: shared listeners, a controller only while the menu is in use) and the look is `@svelte-lean/styles/select.css`; the application imports both, once.
- The label, the listbox and the trigger are tied by ids from `$props.id()`, stable between the server render and hydration.

## Without a pager

Two different things: `pagination={false}` removes the pager and renders the whole model, while a page size of zero keeps the pager (and its size select) and shows every row.

`two ways to show everything`

```svelte
<!-- Every row on one page, no pager. -->
<DataTable {data} {columns} getRowId={(row) => row.id} pagination={false} />

<!-- The pager stays, page size 0 shows every row and the select offers "All". -->
<DataTable {data} {columns} getRowId={(row) => row.id} state={defaultState({ pageSize: 0 })} />
```

## Server pagination

With `manual`, the engine slices nothing: `data` is the page the server returned and `rowCount` is the total it reported. The state still changes when the user pages, so the application requests the next slice from it. An endpoint that reports only whether another page follows (a cursor) passes `pageCount` instead: the status line then leaves the total out, and the `page` message receives `undefined` as its row count. With a known `rowCount` the table also sets `aria-rowcount` and `aria-rowindex`, so assistive technology reads the row position within the whole result. The [Server data](https://svelte-lean.vercel.app/table/server-data.md) page shows the loader and the stale-request guard.

`manual mode`

```svelte
<!-- The page is a slice the server produced; rowCount gives the page count. -->
<DataTable manual data={page.rows} rowCount={page.total} bind:state={tableState} {columns} getRowId={(row) => row.id} />

<!-- The server knows only whether another page follows: pageCount instead of rowCount. -->
<DataTable
	manual
	data={page.rows}
	pageCount={(tableState.pageIndex ?? 0) + (page.hasMore ? 2 : 1)}
	bind:state={tableState}
	{columns}
	getRowId={(row) => row.id}
/>
```

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`pagination`](https://svelte-lean.vercel.app/table/api.md#prop-pagination) | Controlled state | `boolean` | `true` | Shows the pager and slices pages. false renders the whole row model on one page. |
| [`manual`](https://svelte-lean.vercel.app/table/api.md#prop-manual) | Controlled state | `boolean` | `false` | Treats data as already sorted, filtered and paged by the application; the client model applies none of them. |
| [`rowCount`](https://svelte-lean.vercel.app/table/api.md#prop-rowcount) | Controlled state | `number` | `undefined` | Total row count in manual mode, for the page count, the status line and aria-rowcount. Without it the status line leaves the total out. |
| [`pageCount`](https://svelte-lean.vercel.app/table/api.md#prop-pagecount) | Controlled state | `number` | `from rowCount` | Manual mode: the page count when the total is unknown, for example pageIndex + 2 while the server reports a next page. |
| [`pageSizeOptions`](https://svelte-lean.vercel.app/table/api.md#prop-pagesizeoptions) | Controlled state | `number[]` | `[10, 20, 50, 100]` | Sizes offered by the page-size select; the current size is always added. 0 is the all-rows option. |
| [`pageSizeSelect`](https://svelte-lean.vercel.app/table/api.md#prop-pagesizeselect) | Controlled state | `'native' \| 'custom'` | `'native'` | The page-size control. native: a native \<select\>. custom: the Select primitive (a trigger button and a listbox popup drawn the same in every engine) around the same native \<select\>, which keeps the value and works without scripts; needs @svelte-lean/primitives/select/register and @svelte-lean/styles/select.css. |
| [`pageIndex`](https://svelte-lean.vercel.app/table/api.md#state-pageindex) | TableState | `number` | `0` | Zero-based page. |
| [`pageSize`](https://svelte-lean.vercel.app/table/api.md#state-pagesize) | TableState | `number` | `20` | Rows per page; 0 shows every row. |
| [`rowPinning`](https://svelte-lean.vercel.app/table/api.md#state-rowpinning) | TableState | `{ top: RowId[]; bottom: RowId[] }` | `{ top: [], bottom: [] }` | Rows kept at the start or end of every page. |
| [`page`](https://svelte-lean.vercel.app/table/api.md#message-page) | TableMessages | `(page: number, pages: number, rows: number \| undefined) => string` | ``(page, pages, rows) => `Page ${page} of ${pages}` + (rows === undefined ? '' : ` · ${rows} rows`)`` | The pager status line; rows is undefined when a manual table has no rowCount. |
| [`pageNumber`](https://svelte-lean.vercel.app/table/api.md#message-pagenumber) | TableMessages | `(page: number) => string` | `` (page) => `Page ${page}` `` | Accessible name of a numbered page button. |
| [`pageSize`](https://svelte-lean.vercel.app/table/api.md#message-pagesize) | TableMessages | `string` | `'Rows per page'` | Label of the page-size select. |
| [`allRows`](https://svelte-lean.vercel.app/table/api.md#message-allrows) | TableMessages | `string` | `'All'` | The pageSize: 0 option of the page-size select. |
| [`first`](https://svelte-lean.vercel.app/table/api.md#message-first) | TableMessages | `string` | `'First page'` | Pager button. |
| [`previous`](https://svelte-lean.vercel.app/table/api.md#message-previous) | TableMessages | `string` | `'Previous page'` | Pager button. |
| [`next`](https://svelte-lean.vercel.app/table/api.md#message-next) | TableMessages | `string` | `'Next page'` | Pager button. |
| [`last`](https://svelte-lean.vercel.app/table/api.md#message-last) | TableMessages | `string` | `'Last page'` | Pager button. |
| [`setPage`](https://svelte-lean.vercel.app/table/api.md#table-setpage) | Table | `(index: number) => void` | – | Clamped to the page count. |
| [`setPageSize`](https://svelte-lean.vercel.app/table/api.md#table-setpagesize) | Table | `(size: number) => void` | – | 0 shows every row. Resets the page. |
| [`RowModel<T>`](https://svelte-lean.vercel.app/table/api.md#type-rowmodel-t-) | Row model | `{ rows: RowNode<T>[]; total: number; pageCount: number; pageIndex: number; all: RowNode<T>[] }` | – | The page to render, the filtered total and every filtered data row. |

## Related

- [Server data](https://svelte-lean.vercel.app/table/server-data.md)
- [Filtering](https://svelte-lean.vercel.app/table/filtering.md)
- [State persistence](https://svelte-lean.vercel.app/table/state-persistence.md)
