Navigation

Pagination

A React pagination component whose current-page pill slides between numbers, with expandable ellipses that stagger in hidden pages, a compact “3 / 12” variant with rolling digits, arrow-key navigation and an SEO-friendly link mode.

A pager for lists, search results and product grids. The dark current-page pill springs from number to number while the page label inverts beneath it. With many pages, the first and last pages stay pinned, a few siblings flank the current page and the rest collapse into “…” — click an ellipsis and the hidden pages fade in one after another. When space is tight, switch to the compact variant, which shows only “3 / 12” and rolls each digit up or down as you page. The page supports controlled and uncontrolled modes; pass getHref and every page renders as a real link that search engines can crawl.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add pagination.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

24
1
200
500
Active edited usageDefault props
<Pagination/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/pagination.en.json

Or, once registries are configured in components.json, install it as @webberui/pagination.

Usage

import { Pagination } from "@/components/ui/pagination";

const [page, setPage] = React.useState(1);

<Pagination page={page} onPageChange={setPage} total={24} />

// Compact variant: shows only “3 / 12”
<Pagination compact page={page} onPageChange={setPage} total={12} />

// Link mode: every page is an <a href> that search engines can crawl
<Pagination
  defaultPage={1}
  total={24}
  getHref={(p) => `/products?page=${p}`}
/>

Props

PropTypeDefaultDescription
pagenumber—Controlled current page (1-based); when omitted the component manages it (uncontrolled)
defaultPagenumber1Initial page in uncontrolled mode (1-based)
onPageChange(page: number) => void—Called when the page changes, with the new page
totalnumber—Total number of pages (required)
siblingCountnumber1How many pages to show on each side of the current page
boundaryCountnumber1How many pages to pin at the start and the end
compactbooleanfalseCompact variant: only “3 / 12” plus previous/next, with rolling digits
showFirstLastbooleanfalseShow «/» buttons that jump to the first/last page
prevLabelstring"上一頁" (Previous)Previous button text (icon only at phone widths; the text becomes the aria-label)
nextLabelstring"下一頁" (Next)Next button text (icon only at phone widths; the text becomes the aria-label)
getHref(page: number) => string—When provided, every page renders as an <a href>; otherwise buttons
durationnumber200Fade and hover transition time (ms), also written to --wb-pagination-duration
staggernumber30Delay between pages fading in when an ellipsis expands (ms)
stiffnessnumber500Spring stiffness for the current-page pill and rolling digits
dampingnumber35Spring damping for the current-page pill and rolling digits
aria-labelstring"分頁" (Pagination)Accessible name of the nav
classNamestring—Passed through to the outer nav

The pure function getRange(page, total, siblingCount, boundaryCount) is also a named export. It returns (number | "ellipsis-left" | "ellipsis-right")[], so you can reuse the same page algorithm on the server or in a custom layout.

How it works

  • Page algorithm: boundaryCount pages stay pinned at each end and siblingCount pages flank the current page; when total is below boundaryCount × 2 + siblingCount × 2 + 3, every page is shown. Near either end the sibling window shifts inward so the list keeps a constant item count; an ellipsis only appears when it hides at least two pages.
  • The current-page pill is a single element with a shared layoutId; on page change it springs (stiffness/damping) to its new spot. When the list changes, the other items reflow with layout animation and newly shown pages fade in with a slight scale.
  • Clicking “…” replaces it with the pages it stands for, fading them in at stagger intervals starting from the page nearest the current one. If more than 10 pages are hidden, only the nearest 10 expand and an ellipsis remains for the rest — click it again to keep going. The expansion resets after a page change.
  • In the compact variant each digit is its own rolling column, and only the digits that change move: paging forward rolls the new digit up from below, paging back rolls it down from above.
  • With getHref, pages are ordinary links: clicking navigates as usual and also calls onPageChange. With a framework router, handle navigation in onPageChange yourself.
  • Timing variable: the root element sets --wb-pagination-duration (default 200ms), used by the hover background transition; override it in global styles.

Accessibility

  • The root is a <nav> named by aria-label (default “分頁” — Pagination); the current page has aria-current="page" and every page has an aria-label of “Page N” (第 N 頁 in the default Chinese build)
  • Each ellipsis is a button whose aria-label says which pages it will reveal (for example “Show pages 4 to 9”)
  • On the first page Previous, and on the last page Next, use aria-disabled rather than disabled, so they stay focusable and screen readers announce them as dimmed
  • With any pagination button focused, ← / → go to the previous/next page and Home/End jump to the first/last page; focus follows the new current page
  • The compact variant announces “Page 3 of 12” through visually hidden aria-live="polite" text, and the rolling digits are hidden from assistive technology
  • When the user has reduce motion turned on, the pill jumps straight to its new spot, expanded pages appear without staggering and the compact digits swap without rolling