Skip to content
Cynosurev0.5.0
SearchStorybookGitHubnpmColour mode: System. Switch to Light

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.

Landmark and current page

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"

tsx
Copy
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].

Siblings and boundaries

First and lastLink to "First and last"

Previous next and edges

CompactLink to "Compact"

Compact

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.

Links to every page

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.

tsx
Copy
<Pagination
  count={pages}
  page={page}
  getHref={(p) => `/members?page=${p}`}
  onChange={(p, event) => {
    event.preventDefault();
    router.push(`/members?page=${p}`);
  }}
/>
Router prevents the link and navigates itself

PropsLink to "Props"

Every prop of Pagination
PropWhat it doesTypeDefault
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.number1
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.number1
boundaries
Pages always shown at each end. Default 1.number1
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.booleanfalse
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.booleanfalse
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"

Behaviour and accessibility, table 1
InputResult
Click, tap, Enter, SpaceGoes to that page and calls onChange(page, event)
TabMoves between the controls. Disabled ones and ellipses are skipped
  • Landmark: a nav named "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 with getHref), so a screen reader says "Page 3, current page" once. Do not add "current" to labels.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.
Right to left

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-*).

ts
Copy
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.

Table · Button · IconButton · Tabs · Steps

These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.

Getting started
Theming
Icons
Accessibility
Code editors and highlighters
Using Cynosure without React
Build-tool adapters
Components
Accordion
AppShell
AspectRatio
Breadcrumbs
Calendar
Card
Container
Disclosure
Divider
DocumentViewport
Grid
Inline
PageHeader
PanZoomCanvas
ScrollArea
SideNav
SortableList
SplitPane
Stack
StatusBar
Tabs
TitleBar
Toolbar
Tree
VirtualList
ZoomControls
Announcer
Avatar
Badge
CodeBlock
DescriptionList
EmptyState
ErrorBoundary
Field
Heading
IconTile
Image
Kbd
List
LoadingDots
Notice
ProgressBar
Prose
Skeleton
Stat
Steps
Table
Text
Button
Checkbox
Checklist
ColorField
ColorSwatches
Combobox
CompareSlider
CopyButton
DateField
FileDrop
IconButton
KeyValueEditor
Link
NumberField
Pagination
PathField
RadioGroup
SearchField
SegmentedControl
Select
SelectionFrame
SignaturePad
Slider
Switch
TagInput
TextArea
TextField
ThemeToggle
TimeField
ButtonBase
ChoiceBase
DialogBase
FileInputBase
HiddenValue
Icon
InputBase
LinkBase
LiveRegion
TextAreaBase
ChatMessage
Choice
DataGrid
DeviceFrame
HighlightedText
HotkeyField
ListRow
OptionList
OverlayFrame
QRCode
RowActions
Section
SiteFooter
SiteHeader
SkipLink
SortButton
VisuallyHidden
CommandPalette
ContextMenu
Dialog
FloatingToolbar
HoverCard
Menu
Menubar
Popover
ProgressOverlay
Sheet
Toast
Tooltip
Changelog