Pagination
Moves through pages of results.
Sizes
sm
defaultdefault
Mode
551–600 of 60,184 runs
numbereddefault
compact
101–150 events
cursor
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
pagenumberpagesnumberhasNextbooleangetHref(page) => stringhrefPatternstringdefault'?page={page}'onPageChange(page, event) => voidsize'sm' | 'default'default'default'PaginationLink isActive, disabledbooleandefaultfalsePaginationInfo{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