Pagination

URL-driven page controls that sync the current page with the page query parameter in the Next.js App Router.

Pagination is a client component that renders previous/next buttons and a compact list of page numbers, for paged lists and search results. It reads and writes the page search parameter through next/navigation, so it requires the Next.js App Router. Page changes in the demos update ?page= in the address bar.

95 invoices, 10 per pageLast requested page: none yet

Import#

typescript
import { Pagination, PaginationList } from "@reactberry/system/blocks";

Usage#

typescript
"use client";

import { Suspense } from "react";
import { Pagination } from "@reactberry/system/blocks";
import { Box, Text } from "@reactberry/system/elements";

export default function ResultsFooter({ total }: { total: number }) {
  return (
    <Box display="flex" flexDirection="column" gap="s">
      <Text color="secondary">{total} results</Text>
      <Suspense fallback={null}>
        <Pagination
          total={total}
          pageSize={20}
          onPageChange={(page) => console.log("Page", page)}
        />
      </Suspense>
    </Box>
  );
}

Examples#

Page size#

pageSize sets the number of items per page. Nothing is rendered when total fits on one page.

240 orders, 25 per page
8 orders, 25 per page (no controls)

PaginationList#

PaginationList renders children followed by the controls in a sticky footer. It does not slice children; render only the items for the current page.

API#

Pagination#

PropTypeDefaultDescription
total*
number
—

Total number of items across all pages.

pageSize
number
10

Items per page; used to compute the page count.

onPageChange*
(page: number) => void
—

Called with the 1-based target page before the router navigates.

No other props are accepted and nothing is spread.

PaginationList#

PropTypeDefaultDescription
total*
number
—

Passed to Pagination.

pageSize
number
10

Passed to Pagination.

children*
ReactNode
—

The list content rendered above the controls.

onPageChange
(page: number) => void
—

Called with the 1-based page when it changes.

The controls are wrapped in a Box with position="sticky", bottom="0", bg="base" and p="small". No other props are accepted. The source comment mentions a default page size of 20, but the code default is 10.

Accessibility#

  • The controls are wrapped in a nav element without an aria-label.
  • The current page button has aria-current="true" and no click handler. Other page buttons have aria-label values such as "Page 3".
  • The previous and next buttons switch their aria-label to "No previous page available" / "No next page available" on the first and last page.
  • Buttons are Button elements rendered as span with role="button" and tabIndex={0}. They can be focused but have no Enter or Space handling, and disabled only blocks pointer events.

Notes#

  • The current page is read from searchParams.get("page") and defaults to 1. Page numbers are 1-based. Every Pagination on a route reads the same parameter, so the examples on this page share it.
  • Changing page calls onPageChange and then router.push with the current pathname and the updated page parameter; other query parameters are preserved.
  • Up to three page numbers are shown around the current page, with the first and last pages and … placeholders added as needed. Placeholder buttons are disabled.
  • The previous and next buttons are disabled on the first and last page.
  • Because it uses useSearchParams, wrap the component in a Suspense boundary in statically rendered routes.