Table State
Pagination
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.
- Fixture
- Benchmark
On this page
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.
Amara Okafor | Engineering | Europe | $4,501 |
Jonas Lindqvist | Design | Americas | $12,420 |
Mei Tanaka | Operations | Asia | $20,339 |
Rafael Duarte | Growth | Europe | $28,258 |
Selin Aydın | Engineering | Americas | $36,177 |
<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="outline" 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="outline" 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"
/>- Sorting, searching or filtering resets
pageIndexto zero. - The status line reads the filtered total, not the length of
data.
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.
<!-- 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 page shows the loader and the stale-request
guard.
<!-- 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 | Controlled state | boolean | true | Shows the pager and slices pages. false renders the whole row model on one page. |
manual | Controlled state | boolean | false | Treats data as already sorted, filtered and paged by the application; the client model applies none of them. |
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 | 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 | 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. |
pageIndex | TableState | number | 0 | Zero-based page. |
pageSize | TableState | number | 20 | Rows per page; 0 shows every row. |
rowPinning | TableState | { top: RowId[]; bottom: RowId[] } | { top: [], bottom: [] } | Rows kept at the start or end of every page. |
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 | TableMessages | (page: number) => string | (page) => `Page ${page}` | Accessible name of a numbered page button. |
pageSize | TableMessages | string | 'Rows per page' | Label of the page-size select. |
allRows | TableMessages | string | 'All' | The pageSize: 0 option of the page-size select. |
first | TableMessages | string | 'First page' | Pager button. |
previous | TableMessages | string | 'Previous page' | Pager button. |
next | TableMessages | string | 'Next page' | Pager button. |
last | TableMessages | string | 'Last page' | Pager button. |
setPage | Table | (index: number) => void | – | Clamped to the page count. |
setPageSize | Table | (size: number) => void | – | 0 shows every row. Resets the page. |
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. |