PaginationBar
Moves through a long list a page at a time.
Preview
tsx
import { state } from '@kerf-js/core'import { PaginationBar } from '@kerf-js/veneer'export function Offset() { const page = state(6) const size = state(25) return ( <PaginationBar label="Deploys" page={page} size={size} total={2480} /> )}Installation
npm install @kerf-js/veneerTry it
Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.
Live preview
Controls
Total
Size
Codeupdates as you change the controls
import { PaginationBar } from '@kerf-js/veneer'<PaginationBar label="Projects" page={page} total={1240} />Usage
tsx
import { PaginationBar } from '@kerf-js/veneer'Previous and Next
mode="cursor" is for a list with no page count. You move pageIndex.
tsx
import { state } from '@kerf-js/core'import { PaginationBar } from '@kerf-js/veneer'export function Cursor() { const index = state(0) return ( <PaginationBar label="Audit log" mode="cursor" size={50} pageIndex={index.value} hasPrev={index.value > 0} hasMore={index.value < 3} onPageChange={(i) => (index.value = i)} /> )}In a small space
Under 520px of its own width only the arrows and the current page stay.
tsx
import { state } from '@kerf-js/core'import { PaginationBar } from '@kerf-js/veneer'export function Narrow() { const page = state(4) const size = state(25) return ( <PaginationBar label="Deploys" page={page} size={size} total={2480} /> )}Moved from outside
Pass a state as page and write to it from anywhere.
tsx
import { state } from '@kerf-js/core'import { Button, PaginationBar, Row, Stack, Text} from '@kerf-js/veneer'export function Controlled() { const page = state(8) const size = state(10) return ( <Stack gap="related"> <PaginationBar label="Projects" page={page} size={size} total={240} /> <Row gap="related"> <Button size="sm" emphasis="outline" tone="neutral" onClick={() => (page.value = 1)} > Back to the start </Button> <Text subtle> Page {page.value}, set from outside </Text> </Row> </Stack> )}Paging a list
Pass page, size, sizes and total to move through a long list a few rows at a time.
tsx
import { state } from '@kerf-js/core'import { Badge, PaginationBar, Row, Stack, Text} from '@kerf-js/veneer'export function List() { const page = state(1) const size = state(4) const rows = () => PROJECTS.slice( (page.value - 1) * size.value, page.value * size.value, ) return ( <Stack gap="group"> <Stack gap="related" divided > {rows().map((p) => ( <Row key={p.id} justify="between" gap="group" > <Stack gap="hair"> <Text>{p.name}</Text> <Text role="caption" subtle > {p.owner}, {p.region} </Text> </Stack> <Badge tone={TONE_OF[p.status]} dot > {p.status} </Badge> </Row> ))} </Stack> <PaginationBar label="Projects" page={page} size={size} sizes={[4, 6, 10]} total={PROJECTS.length} /> </Stack> )}type Status = | 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string name: string owner: string region: string status: Status deploys: number uptime: number latency: number cost: number updated: string}[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31', }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31', }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30', }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29', }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29', }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27', }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22', }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31', }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28', }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30', },]const TONE_OF: Record< Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral',}When to use
Use it for
- A list people jump around in, or link to a page of
- You know the total, or at least whether there is more
Reach for something else
- LoadMore, when people read straight down a feed
- VirtualList, when it is one long list that scrolls
Accessibility
- Tab
- Moves between the page buttons
- Enter
- Goes to that page, or the number typed in Go to
- After a press, focus moves to the new current page, so it is never lost when a button goes away.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Pagination' | The name of the navigation landmark. |
mode | "cursor" | "offset" | 'offset' | offset (default): numbered pages. cursor: Previous and Next, for a list with no page count. |
page | number | State<number> | — | 1-based. |
defaultPage | number | — | The page it opens on when page isn't given. |
total | number | 0 | How many rows there are, in numbered mode. |
size | number | State<number> | — | Rows per page. Pass a state so the rows-per-page select can change it. |
sizes | number[] | [10, 25, 50, 100] | What the rows-per-page select offers. |
hasMore | boolean | false | Cursor mode: enables Next. |
hasPrev | boolean | false | Cursor mode: enables Previous. |
pageIndex | number | 0 | Cursor mode: which page this is, from 0. |
supply | number | — | Cursor mode: the total, when you happen to know it. |
totalFetched | number | — | Cursor mode: how many rows are showing, when pages differ in size. |
locale | string | — | The locale its numbers are written in. en-US without it. |
| Event | Type | Default | Description |
|---|---|---|---|
onPageChange | ((page: number) => void) | — | Hears the new page (offset) or page index (cursor) on every move. |
onSizeChange | ((size: number) => void) | — | Hears a new rows-per-page choice; the bar goes back to page 1. |