Skip to content
kerfjsGitHub

PaginationBar

Moves through a long list a page at a time.

SourcePackage @kerf-js/veneerTry it

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/veneer

Try 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
Total1240
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

PropTypeDefaultDescription
labelstring'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.
pagenumber | State<number>—1-based.
defaultPagenumber—The page it opens on when page isn't given.
totalnumber0How many rows there are, in numbered mode.
sizenumber | State<number>—Rows per page. Pass a state so the rows-per-page select can change it.
sizesnumber[][10, 25, 50, 100]What the rows-per-page select offers.
hasMorebooleanfalseCursor mode: enables Next.
hasPrevbooleanfalseCursor mode: enables Previous.
pageIndexnumber0Cursor mode: which page this is, from 0.
supplynumber—Cursor mode: the total, when you happen to know it.
totalFetchednumber—Cursor mode: how many rows are showing, when pages differ in size.
localestring—The locale its numbers are written in. en-US without it.
EventTypeDefaultDescription
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.

RelatedLoad moreTableData grid