Easy Shadcn
Components

Pagination

Pagination flattens shadcn's navigation, content, item, link, Previous, Next, and ellipsis parts into one item-total-driven control. It supports client-side page changes and genuine route links without mixing the two destination models.

Client controlled

Current page: 4

Client uncontrolled

Last reported page: 3

Route navigation

Genuine links preserve native browser navigation.

Compact range

Disabled

Presentation

Installation

With the @easy-shadcn namespace configured:

pnpm dlx shadcn@latest add @easy-shadcn/pagination

Or install via the full URL (zero configuration):

pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/pagination.json

The underlying shadcn pagination primitive is installed automatically.

Client paging

Client mode requires onValueChange. Use value when application state owns the current page:

import { Pagination } from "@/components/easy/pagination"

<Pagination
  total={238}
  pageSize={25}
  value={page}
  onValueChange={setPage}
/>

Omit value and provide an optional defaultValue when Pagination should own the highlighted page:

<Pagination
  total={238}
  defaultValue={2}
  onValueChange={(nextValue) => loadPage(nextValue)}
/>

Client controls deliberately have no href. They expose button semantics, activate through click, Enter, and Space, and emit only normalized in-range integers. A controlled value remains authoritative until its caller rerenders; an uncontrolled value updates before onValueChange is reported.

Route navigation

Navigation mode requires a route-derived value and getPageHref. It accepts neither defaultValue nor onValueChange:

<Pagination
  total={238}
  pageSize={25}
  value={pageFromSearchParams}
  getPageHref={(nextPage) => `/reports?page=${nextPage}`}
/>

Numbered pages and available Previous and Next targets are genuine links. Pagination does not intercept their click or keyboard events, so modified clicks, middle clicks, copy-link, context menus, and open-in-new-tab remain native. getPageHref must be pure, deterministic, non-throwing, and return a non-empty string for every rendered target page.

Props

PropTypeDefaultDescription
totalnumberrequiredTotal item count from which the page count is derived.
pageSizenumber10Number of items represented by each page.
valuenumber-Controlled current page in client-controlled or navigation mode.
defaultValuenumber1Initial page in client-uncontrolled mode; mutually exclusive with value and getPageHref.
onValueChange(value: number) => voidrequired in client modeReports user-activated client destinations; mutually exclusive with getPageHref.
getPageHref(page: number) => stringrequired in navigation modeProduces genuine route destinations; requires value and rejects client state props.
boundaryCountnumber1Number of pages always shown at each outer edge, capped at 100.
siblingCountnumber1Number of pages shown on each side of the effective current page, capped at 100.
disabledbooleanfalseKeeps Pagination visible while making every generated control inert and non-navigable.
hideOnSinglePagebooleanfalseRemoves the complete navigation landmark when the derived page count is one.
classNameClassValue-Class override for the Pagination navigation root.
contentClassNameClassValue-Class override for PaginationContent.
itemClassNameClassValue-Class added to every PaginationItem, including Previous, numbered, ellipsis, and Next wrappers.
linkClassNameClassValue-Class added only to numbered PaginationLink leaves.
previousClassNameClassValue-Class override for PaginationPrevious; does not inherit linkClassName.
nextClassNameClassValue-Class override for PaginationNext; does not inherit linkClassName.
ellipsisClassNameClassValue-Class override for PaginationEllipsis.
refReact.Ref<HTMLElement>-Ref to the navigation root.

Other safe navigation props are forwarded, including id, style, aria-label, aria-labelledby, data attributes, and native root event handlers.

Numeric normalization

All finite numeric inputs are truncated with Math.trunc before calculations. total, pageSize, value, and defaultValue are capped at Number.MAX_SAFE_INTEGER, so generated page numbers and activation targets are always safe integers. Window counts use a smaller upper bound to keep rendering bounded:

  • total clamps to at least 0; non-finite values fall back to 0.
  • pageSize clamps to at least 1; non-finite values fall back to 10.
  • value and defaultValue clamp first to at least 1 and then to the derived page count; non-finite values fall back to 1.
  • boundaryCount and siblingCount clamp between 0 and 100; non-finite values fall back to 1.

The derived page count is always at least one. Controlled out-of-range input is clamped only for rendering and interaction targets; prop changes never emit a corrective callback. When total or pageSize shrinks, uncontrolled state silently settles on the new maximum. Growing the page count later therefore cannot restore a stale out-of-range page.

Compact range

Pagination merges the left boundary, current-page sibling window, and right boundary without allocating or iterating over every page. Work stays proportional to the capped boundaryCount + siblingCount, overlapping ranges are deduplicated, and pages remain ordered. A real gap uses PaginationEllipsis; if exactly one page would be hidden, that page is rendered instead.

Accessibility and disabled behavior

The root is a navigation landmark named pagination by default. Override aria-label, or provide aria-labelledby, when a page contains multiple pagination landmarks. The effective current page exposes aria-current="page".

Unavailable Previous and Next controls omit href, expose aria-disabled="true", leave the tab order, and ignore client activation. With disabled, the same contract applies to every generated control in both modes; route hrefs are not produced. hideOnSinglePage removes the entire landmark rather than rendering empty navigation markup.

The primitive's built-in English Previous and Next text and accessible labels are preserved. Localization remains a copy-in concern: edit the installed source, wrap it with project defaults, or compose the primitives directly.

Ownership and server rendering

Compose owns the complete generated children, raw HTML, navigation role, primitive slot marker, link destinations, current and disabled markers, tab order, and client interaction handlers. These keys cannot be replaced through typed props, and hostile runtime spreads cannot overwrite the generated control semantics.

Markup depends only on props and the deterministic href mapper, so server and client rendering remain stable. Pagination does not read browser globals, parse URLs, move focus, fetch data, or slice item arrays.

When to use the primitive

Use components/ui/pagination directly for page-size changers, quick jumpers, simple or responsive layouts, first/last action buttons, router components, custom item rendering, alternate Previous/Next/ellipsis copy, arbitrary item structures, render props, prop bags, or per-page styling callbacks. Pagination intentionally keeps those cases in primitive composition rather than expanding the flat API.

On this page