Pagination
- Stable
- WCAG 2.2 evidence
- RTL
Moves through the pages of a list or table. A nav landmark with a named list of page
buttons or links: the current page has aria-current="page", long lists collapse into ellipses (siblingCount,
boundaryCount), previous, next, first and last have names, and controls at the end of the list stay focusable with
aria-disabled so keyboard focus is not lost (WCAG 1.3.1, 2.1.1, 2.4.1, 2.4.7, 4.1.2). When the page changes without
navigation, a polite status message names the new page (4.1.3). Targets are 32 px (40 px for mobile and load-more),
44 pt and 48 dp on touch platforms (2.5.8). Use it for tables and lists with stable page numbers; use CursorPager for cursor-based data and FeedStatus for
infinite feeds. Consumer duties: a unique landmark name, the translated range text, and updating the content (and
moving focus to it where that helps) when the page changes.
import { Pagination } from "@nexera-ui/react";- 9
- examples
- 21
- props
- 1
- live controls
- 1
- platform
- 11
- WCAG criteria
- 0
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { Pagination } from "@nexera-ui/react";
<Pagination />Examples 8
The same examples as Storybook, rendered live. Open Code to copy one.
Types
Figma Type=Compact, Simple and Mobile, each wired to a page state.
import { useState } from "react";
import { Pagination } from "@nexera-ui/react";
const TOTAL = 128;
function rangeOf(page: number, size: number): string {
const from = (page - 1) * size + 1;
return `${String(from)}-${String(Math.min(page * size, TOTAL))} of ${String(TOTAL)}`;
}
function Interactive({ type = "full" }: { type?: Exclude<PaginationType, "load-more"> }) {
const [page, setPage] = useState(2);
const [size, setSize] = useState(10);
const pageCount = Math.ceil(TOTAL / size);
return (
<Pagination
aria-label="Employees"
type={type}
page={Math.min(page, pageCount)}
pageCount={pageCount}
onPageChange={setPage}
range={rangeOf(Math.min(page, pageCount), size)}
caption={`${String(TOTAL)} employees`}
pageSize={size}
onPageSizeChange={(next) => {
setSize(next);
setPage(1);
}}
/>
);
}
export function Types() {
return (
<div className="flex max-w-xl flex-col gap-6">
<Interactive type="compact" />
<Interactive type="simple" />
<Interactive type="mobile" />
</div>
);
}
Load more
Figma Type=Load more: appends content in place, keeps focus on the button and announces the new range.
import { useState } from "react";
import { Pagination } from "@nexera-ui/react";
const TOTAL = 128;
export function LoadMore() {
function Feed({ isLoading }: { isLoading?: boolean }) {
const [loaded, setLoaded] = useState(20);
return (
<Pagination
aria-label="Employees"
type="load-more"
range={`1-${String(loaded)} of ${String(TOTAL)}`}
progress={(loaded / TOTAL) * 100}
{...(isLoading === undefined ? {} : { isLoading })}
onLoadMore={() => {
setLoaded(Math.min(TOTAL, loaded + 20));
}}
/>
);
}
return (
<div className="flex max-w-md flex-col gap-4">
<Feed />
<Feed isLoading />
</div>
);
}
Collapsing
siblingCount and boundaryCount decide how many numbers show; the list keeps the same length while paging. The first row is the default (1 and 1), the second shows only the current page between the boundaries, the third keeps two at each end.
import { Pagination } from "@nexera-ui/react";
export function Collapsing() {
return (
<div className="flex flex-col gap-2">
<Pagination aria-label="Default counts" page={10} pageCount={30} />
<Pagination aria-label="No siblings" page={10} pageCount={30} siblingCount={0} />
<Pagination aria-label="Wide" page={10} pageCount={30} siblingCount={2} boundaryCount={2} />
</div>
);
}
Ends
The ends of the list: Previous and First are aria-disabled but stay focusable on page 1; Next and Last on the last page.
import { Pagination } from "@nexera-ui/react";
export function Ends() {
return (
<div className="flex flex-col gap-2">
<Pagination aria-label="First page" page={1} pageCount={13} />
<Pagination aria-label="Last page" page={13} pageCount={13} />
</div>
);
}
Page links
With getPageHref the controls are links (server-rendered or routed pages); nothing is announced because navigation is.
import { Pagination } from "@nexera-ui/react";
export function PageLinks() {
return (
<Pagination
pageCount={13}
page={2}
aria-label="Employees (links)"
getPageHref={(page) => `#page-${String(page)}`}
/>
);
}
Narrow width
The Figma Pagination is 1000 px wide; here the row wraps inside a 320 px column instead of overflowing.
import { Pagination } from "@nexera-ui/react";
export function NarrowWidth() {
return (
<div className="max-w-xs">
<Pagination
aria-label="Employees (narrow)"
page={6}
pageCount={13}
siblingCount={0}
range="51-60 of 128"
pageSize={10}
defaultPage={6}
/>
</div>
);
}
Long labels
Long translated strings and a long range wrap; nothing is truncated.
import { Pagination } from "@nexera-ui/react";
export function LongLabels() {
return (
<div className="flex max-w-sm flex-col gap-4">
<Pagination
aria-label="Mitarbeiterverzeichnis"
type="simple"
page={2}
pageCount={13}
range="Einträge 11 bis 20 von insgesamt 128 Mitarbeitenden"
labels={{ previousText: "Vorherige Seite", nextText: "Nächste Seite" }}
/>
<Pagination
aria-label="Mitarbeiterverzeichnis (mobil)"
type="mobile"
page={2}
pageCount={13}
range="Einträge 11 bis 20 von 128"
caption="128 Mitarbeitende in Berlin, München und Hamburg"
/>
</div>
);
}
Right to left
Right to left: the list starts at the right, the glyphs mirror and Next points to the left.
import { Pagination } from "@nexera-ui/react";
export function RightToLeft() {
return (
<div className="flex flex-col gap-4">
<Pagination
aria-label="الموظفون"
page={5}
pageCount={20}
range="١١–٢٠ من ١٢٨"
defaultPageSize={20}
labels={{
previous: "الصفحة السابقة",
next: "الصفحة التالية",
first: "الصفحة الأولى",
last: "الصفحة الأخيرة",
page: (page) => `صفحة ${String(page)}`,
rowsPerPage: "الصفوف في الصفحة",
}}
/>
<Pagination aria-label="الموظفون (مضغوط)" type="compact" page={2} pageCount={13} />
</div>
);
}
Props 21
Press "Try it" on a card to load that prop into the playground.
21 props shown
pageNexeranumberCurrent page, from 1 (controlled). Use with onPageChange. Ignored by type="load-more".
defaultPageNexeranumberPage on first render (uncontrolled).
onPageChangeNexera(page: number) => voidCalled with the new page when a control or page button is pressed.
rangeNexeraReactNodeText of the range, for example "11-20 of 128". You compute and
translate it, so numbers, plural forms and digits match your locale. Shown by every type; it is announced politely
when it changes with type="load-more".
captionNexeraReactNodeSecond line of the label under the range, type="mobile" only.
siblingCountNexeranumberPage numbers shown on each side of the current page (type="full"). Lower it where space is short; the list always keeps
the same length while the user pages through it. Use {@link getPaginationRange } to see the entries.
boundaryCountNexeranumberPage numbers always shown at the start and at the end (type="full").
getPageHrefNexera(page: number) => stringMakes the page controls links: getPageHref(page) is each address, for server-rendered or routed pages. Without it the
controls are buttons and the page changes in place, which Pagination announces. Navigation announces itself.
pageSizeNexeranumberRows per page (controlled, type="full"). Setting it (or defaultPageSize) shows the Figma Rows per page select;
leave both out to hide it. Reset the page in your handler when the size changes.
defaultPageSizeNexeranumberRows per page on first render (uncontrolled), which also shows the select.
onPageSizeChangeNexera(pageSize: number) => voidCalled with the size the user picked.
pageSizesNexerareadonly number[]The choices of the rows-per-page select. The current size is added when missing.
onLoadMoreNexera() => voidtype="load-more": called when the button is pressed.
isLoadingNexerabooleantype="load-more": shows the spinner in the button, blocks presses and announces the state (Button isLoading).
progressNexeranumbertype="load-more": the share of items loaded, 0 to 100. Shown as a decorative 200 x 4
bar; the range text carries the same information for assistive technology.
labelsNexeraPartial<PaginationLabels>Strings the pagination invents (previous, next, first, last, page names, button texts). Translate them.
getPageAnnouncementNexera(page: number, pageCount: number) => stringText announced politely after the page changed in place (not for page links, nor type="load-more").
typeNexera"full" | "compact" | "simple" | "load-more" | "mobile"Layout: full (rows per page, range, page numbers), compact (range, previous and next icons),
simple (range, Previous and Next buttons), load-more (range, progress, Load more button) and mobile
(previous, range label, next). mobile is a layout here, not a platform: touch sizes follow NexeraProvider.
pageCountNexeranumberNumber of pages. Not needed by type="load-more".
aria-labelNexerastringAccessible name of the pagination landmark, for example "Employees". Say what is paged: a page with several nav
landmarks needs a unique name for each. Translate it.
Accessible name when it must differ from the visible text.
aria-labelledbyNexerastringId(s) of visible element(s) that name the landmark; wins over aria-label.
Id(s) of visible element(s) that name the landmark, for example the heading of the table.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 3 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.1Bypass BlocksLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.4Link Purpose (In Context)Level A · tested directly
- 2.5.3Label in NameLevel A · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-hovereddata-pressed
<Pagination className="data-hovered:opacity-90 shadow-sm" />Used in blocks
No block uses Pagination yet.
Related components
- BreadcrumbShows where the current page sits in the hierarchy and links back to each level.
- BreadcrumbItemOne level of a `Breadcrumb` trail; use it only inside `Breadcrumb`.
- CursorPagerA previous and next pair for data without stable page numbers, such as an activity log or a cursor API.
- FABThe main action of a mobile screen: a floating button with a `nav/active-bg` fill and a large shadow.
- FeedStatusThe status line at the bottom of an infinite feed or a lazy list: "Loading more", "You have reached the end" or "Could not load more" with a retry action.
- JumpToPageA small "Go to page [48] of 120" control for long paged lists.
- NavigationBarThe bottom bar of a mobile layout: a named `nav` landmark with a list of three to five `NavItem`s.
- NavItemOne destination or action of a `NavigationBar`.