sveltelean Table
Versionv0.2.0 GitHub

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
Page 1 of 8
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
Page 1 of 8 · 36 rows

Pagination.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="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"
/>
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.

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
<!-- 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.

manual mode
<!-- 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

NameOfTypeDefaultDescription
paginationControlled statebooleantrueShows the pager and slices pages. false renders the whole row model on one page.
manualControlled statebooleanfalseTreats data as already sorted, filtered and paged by the application; the client model applies none of them.
rowCountControlled statenumberundefinedTotal row count in manual mode, for the page count, the status line and aria-rowcount. Without it the status line leaves the total out.
pageCountControlled statenumberfrom rowCountManual mode: the page count when the total is unknown, for example pageIndex + 2 while the server reports a next page.
pageSizeOptionsControlled statenumber[][10, 20, 50, 100]Sizes offered by the page-size select; the current size is always added. 0 is the all-rows option.
pageIndexTableStatenumber0Zero-based page.
pageSizeTableStatenumber20Rows per page; 0 shows every row.
rowPinningTableState{ top: RowId[]; bottom: RowId[] }{ top: [], bottom: [] }Rows kept at the start or end of every page.
pageTableMessages(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.
pageNumberTableMessages(page: number) => string(page) => `Page ${page}`Accessible name of a numbered page button.
pageSizeTableMessagesstring'Rows per page'Label of the page-size select.
allRowsTableMessagesstring'All'The pageSize: 0 option of the page-size select.
firstTableMessagesstring'First page'Pager button.
previousTableMessagesstring'Previous page'Pager button.
nextTableMessagesstring'Next page'Pager button.
lastTableMessagesstring'Last page'Pager button.
setPageTable(index: number) => void–Clamped to the page count.
setPageSizeTable(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.