Components / Controls
Pagination
Moves between the pages of a list or table: previous and next, numbered pages, and the current page marked. It is a navigation landmark built from Button and IconButton.
When to useLink to "When to use"
- Below a Table or a list whose rows are loaded a page at a time.
variant="compact": on phones and in tight spaces such as a panel footer. It shows only previous and next around "Page 3 of 10".getHref: when each page has its own address (search results, an archive). The controls are real links, so they work without JavaScript and can be opened in a new tab.showEdges: when people often jump to the first or last page.
Use something else when:
- the choices are views of the same content, not pages of a list: Tabs or SegmentedControl
- it is progress through a task with named steps: Steps
- the whole list is already loaded and long: VirtualList scrolls it without pages
UsageLink to "Usage"
import { Pagination, pageCount } from "@arshad-shah/cynosure";
<Pagination count={pageCount(members.length, 25)} page={page} onChange={setPage} />
<Pagination count={10} defaultPage={3} showEdges />
<Pagination count={10} variant="compact" />
<Pagination count={10} page={page} getHref={(p) => `?page=${p}`} />count is the number of pages. pageCount(total, pageSize) works it out from a number of items and never returns less than 1. With one page or fewer, Pagination renders nothing.
page is controlled and defaultPage is not; both count from 1 and are clamped to the pages that exist.
Which pages are shownLink to "Which pages are shown"
boundaries pages are always shown at each end and siblings each side of the current one; the rest is an ellipsis. The row has the same number of items on every page, so the controls do not move under the pointer, and an ellipsis never stands for a single page.
pageRange(page, count, siblings, boundaries) is the function that decides this, exported for your own layouts: pageRange(5, 10) is [1, "gap", 4, 5, 6, "gap", 10].
First and lastLink to "First and last"
CompactLink to "Compact"
LinksLink to "Links"
With getHref, every page number and every control that leads to another page is a link to it. The current page is a link to its own address, marked with aria-current="page". onChange still fires on a plain click; a click with Ctrl, Cmd, Shift or Alt is left to the browser. Only the disabled ends stay buttons, because they lead nowhere.
With a client-side routerLink to "With a client-side router"
onChange is called with the page and the click event. Call event.preventDefault() to stop the browser following the link, then navigate yourself. The links still work without JavaScript and open in a new tab.
<Pagination
count={pages}
page={page}
getHref={(p) => `/members?page=${p}`}
onChange={(p, event) => {
event.preventDefault();
router.push(`/members?page=${p}`);
}}
/>PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
countRequired | How many pages there are. From a number of items, use pageCount(total, pageSize). One page or fewer renders nothing. | number | |
page | The current page, from 1 (controlled). Values outside 1 to count are clamped. | number | |
defaultPage | The page shown first when uncontrolled. Default 1. | number | 1 |
onChange | Called with the new page when a control is pressed, and with the click event. With getHref, a client-side router calls event.preventDefault() here to stop the browser following the link, then navigates itself. | ((page: number, event: MouseEvent<HTMLElement>) => void) | |
siblings | Pages shown each side of the current one. Default 1. | number | 1 |
boundaries | Pages always shown at each end. Default 1. | number | 1 |
variant | pages shows numbered pages; compact shows only previous and next around "Page 3 of 10", for phones and tight spaces. Default "pages". | "pages" | "compact" | "pages" |
showEdges | Adds first-page and last-page controls. Default false. | boolean | false |
getHref | The address of a page. When given, every page number and every control that leads to another page is a real link to it (works without JavaScript, opens in a new tab); the current page is a link with aria-current="page". onChange still fires on a plain click. | ((page: number) => string) | |
disabled | Disables every control. Default false. | boolean | false |
size | Height and text size of the controls. Default "md". | "sm" | "md" | "md" |
labels | Replaces the built-in English strings. | Partial<PaginationLabels> | |
className | Added to the nav element, for layout. | string | |
style | Layout only. Restyle through --cy-pagination-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
| Input | Result |
|---|---|
| Click, tap, Enter, Space | Goes to that page and calls onChange(page, event) |
| Tab | Moves between the controls. Disabled ones and ellipses are skipped |
- Landmark: a
navnamed "Pagination" (labels.nav) holding an ordered list. Give each one its own name when a page has several. - Current page: named "Page 3" and marked with
aria-current="page"(on the button, or on the link withgetHref), so a screen reader says "Page 3, current page" once. Do not add "current" tolabels.page. - Ends: previous and first are disabled on page 1, next and last on the last page.
- Ellipses are hidden from assistive technology and cannot be focused. The page names on each side already say what is left out.
- No arrow keys: there is no ARIA pattern for pagination, so the controls are plain buttons.
- Focus: the pressed control keeps focus when the numbers move along. When it can no longer hold focus (previous on reaching page 1), focus goes to the current page, or in the compact variant to the control that still works.
- Compact: the "Page 3 of 10" text is a polite live region, so a change of page is announced.
- Narrow spaces: the row wraps instead of overflowing.
- Right to left: the order follows the reading direction and the chevrons are mirrored.
- Strings: every built-in string is English and can be replaced through
labels.
TouchLink to "Touch"
Each control has a hit area of at least 24px, growing to 44px on coarse pointers (phones and tablets) without changing its visible size. On coarse pointers the space between controls grows so the hit areas do not overlap; the row wraps sooner, which is what variant="compact" is for.
ThemingLink to "Theming"
Tokens: --cy-pagination-gap (space between the controls), --cy-pagination-page-padding (inline padding of a page number) and --cy-pagination-muted-color (the ellipsis and the "Page 3 of 10" text). The controls themselves follow Button's tokens (--cy-button-*).
createTheme({ components: { pagination: { gap: "2px", pagePadding: "8px" } } });className and style are for layout only (margin, placement). Change the look through tokens so every product stays consistent.
RelatedLink to "Related"
Table · Button · IconButton · Tabs · Steps
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.