Pagination

Moves through pages of results.

Sizes

Mode

States

  • rest

  • hover

  • focus-visible

  • current

  • disabled

Usage

When to use it
Use
A list too long for one screen whose pages have addresses: runs, logs, audit events.
Use
The footer row of a Table or DataGrid card, at size="sm".
Use
A cursor API with no total: leave out pages and pass hasNext.
Not for
Rows the grid holds itself: DataGrid pages them with pageSize. Use Data grid.
Not for
A feed that loads more as the person reads; use a Load more Button at the end. Use Button.
Not for
Switching between two to five views. Use Segmented control.

Rules

6 rules
Must
Give every page an href, with getHref or hrefPattern; pass onPageChange only to route on the client. Because a page link without an href is not a link: it cannot open in a new tab and a screen reader calls it text.
Must
Use PaginationNav for pages from data; never build the number row by hand. Because PaginationNav keeps seven slots of one width, so Next never moves under the pointer, and it falls back to "Page 4 of 24" when the row does not fit.
Never
Put glass, a Button or an ink fill on the current page; isActive gives it the pressed wash and aria-current. Because the current page is where the person is, not a thing to press, and glass there would compete with the page's one primary.
Must
Set size="sm" when the pagination sits on a card's footer row. Because the footer row is 26px plus the shell's inset, and a 32px link pushes the shell open.
Should
Pair the links with PaginationInfo, the range and the total, at the start of the row. Because the numbers say which page, the range says which records, and only the range answers "how many are there".
Must
With a cursor API, leave out pages and pass hasNext; never invent a total. Because a guessed page count sends the person to pages that do not exist.

State order

5 states, highest first
1
disabled
2
current
3
focus-visible
4
hover
5
rest

Accessibility

5 checks
The nav is named "Pagination"; each number is a link named "Page 4", the current one with aria-current="page".
Previous and Next are named "Previous page" and "Next page" when only their arrows show.
A page that cannot be reached keeps its place, takes aria-disabled and leaves the tab order (no href).
Focus is the 2px ink ring inside the pill, so a clipping footer row never cuts it.
Wrap PaginationInfo in role="status" where a page change should be announced without a navigation.

Keyboard

2 keys
Tab
Moves through Previous, the page links and Next; a disabled end is skipped.
Enter
Follows the link.

Edge cases

5 edge cases
1,204 pages
Every slot is four digits wide on every page, so Next stays put.
Narrow box
The numbers give way to "Page 4 of 24" between the arrows; the arrows never leave the box.
One page
Both arrows are disabled; DataGrid hides the links and keeps the range.
Right to left
The row and the arrows mirror.
Under 448px of room
Previous and Next drop their words and keep their names.

API

9 props
page
number
pages
number
hasNext
boolean
getHref
(page) => string
hrefPattern
string default '?page={page}'
onPageChange
(page, event) => void
size
'sm' | 'default' default 'default'
PaginationLink isActive, disabled
boolean default false
PaginationInfo
{from, to, total?, noun?}

Tokens

14 tokens
--control
32px
--control-sm
26px
--radius-pill
999px
--hover
rgb(5 5 5 / 0.06)
--pressed
rgb(5 5 5 / 0.10)
--text-1
rgb(5 5 5 / 0.80)
--text-2
rgb(5 5 5 / 0.60)
--text-3
rgb(5 5 5 / 0.45)
--text-disabled
rgb(5 5 5 / 0.32)
--type-label
500 13px/1.35 var(--font-sans)
--type-label-sm
500 12px/1.35 var(--font-sans)
--type-caption
400 12px/1.35 var(--font-sans)
--focus
rgb(5 5 5 / 0.80)
--dur-state
160ms

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.