Pagination
Move through pages of results and show the current page.
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use pagination when position matters: tables, logs and search results that people come back to, share by URL, or count through. Don't use it for a feed people skim, where infinite scroll or a "Load more" button reads better, or for a list short enough to show whole.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
paginationRange,
} from '@youj/react'
export function Pager({ page, pageCount }: { page: number; pageCount: number }) {
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href={`?page=${page - 1}`} disabled={page === 1} />
</PaginationItem>
{paginationRange(page, pageCount).map((item) =>
typeof item === 'number' ? (
<PaginationItem key={item}>
<PaginationLink href={`?page=${item}`} isActive={item === page} aria-label={`Page ${item}`}>
{item}
</PaginationLink>
</PaginationItem>
) : (
<PaginationItem key={item}>
<PaginationEllipsis />
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext href={`?page=${page + 1}`} disabled={page === pageCount} />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Every page is a real link, so pages can be bookmarked, shared and opened in a new tab. To page without a navigation, handle onClick, call event.preventDefault() and update your state, as the Demo story does. The raised key under the current page is one indicator for the whole control, and it glides to whichever link has isActive.
Description
An activity log of 320 events paged five at a time, with numbered links and previous and next.
On the first page, PaginationPrevious takes disabled, and on the last page PaginationNext does. A disabled link renders without an href and with aria-disabled, so it can't be followed, but it stays in the tab order: paging onto the last page with Enter keeps focus on Next instead of dropping it. Below the sm breakpoint, Previous and Next drop their text and keep their chevrons. Change the text with text.
Page numbers built from paginationRange, with ellipses for the gaps.
paginationRange(page, pageCount, siblings = 1) returns the items to render: the first and last pages, siblings pages either side of the current one, and 'start-ellipsis' or 'end-ellipsis' for the gaps. When every page fits, it returns them all. Once pages are skipped, it always returns 5 + 2 × siblings items, so the control keeps its width while you page and the key never chases a moving target.
paginationRange(10, 20) // [1, 'start-ellipsis', 9, 10, 11, 'end-ellipsis', 20]
paginationRange(120, 240, 2) // [1, 'start-ellipsis', 118, 119, 120, 121, 122, 'end-ellipsis', 240]Pagination with only previous and next, for places with no room for page numbers.
Leave the page numbers out when there's no room for them, as in a card footer, or when the total isn't known, as with a cursor-based API. When text relabels an edge, give it an aria-label that starts with the same word, so what people hear matches what they see. With no page marked current, the raised key stays hidden.
Code
pagination-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsisActive, size, disabled… (7)
Props
PaginationLink
| Prop | Type | |
|---|---|---|
isActive | boolean | – |
disabled | booleanThe edge of the range: aria-disabled, no href and inert to clicks, but still focusable, so paging onto the last page with Enter keeps focus on the control instead of dropping it to the body. | – |
size | 'default' | 'icon'default 'icon' | 'icon' |
PaginationPrevious and PaginationNext
They take every PaginationLink prop, and text for the label beside the chevron.
| Prop | Type | |
|---|---|---|
isActive | boolean | – |
disabled | booleanThe edge of the range: aria-disabled, no href and inert to clicks, but still focusable, so paging onto the last page with Enter keeps focus on the control instead of dropping it to the body. | – |
size | 'default' | 'icon' | – |
text | string | – |
Other parts
Pagination is a nav named "Pagination", PaginationContent a ul, PaginationItem an li and PaginationEllipsis a span. Each takes the props of its element.
MotionCurrent page, previous and Next, hover… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Current pagethe raised key glides to the new page, transform only | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Previous and Nextthe chevron nudges 2px toward where it goes, on hover | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Hovercolor and background | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Pressscales to 0.97 while held | duration.instantease.standard80ms · cubic-bezier(0.2, 0, 0, 1) | 80mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
The key jumps to the new page, the chevrons shift without easing, and links don't scale on press.
In right-to-left layouts the chevrons point, and nudge, the other way.
Reduced motion
Paging works the same. The raised key jumps to the new page instead of gliding, the chevrons shift without easing, and links don't scale on press.
AccessibilityKeyboard, ARIA, focus
Accessibility
Paginationis anavlandmark. Its default name is "Pagination"; give it a more specificaria-label("Invoice pages") when a page has more than one.- The current page's link carries
aria-current="page". - Give number links an
aria-labelsuch as "Page 3", so they're announced as pages, not bare numbers. Previous and Next are named "Go to previous page" and "Go to next page". - Ellipses and the raised key are hidden from assistive technology. The numbers either side of a gap already say pages were skipped.
- Tab moves through the links, and Enter follows one. A disabled Previous or Next is still a Tab stop, announced as unavailable, so focus never falls back to the page.