Navigation

Pagination

Page navigation for a long list: Previous, the page numbers with gaps, and Next, with the current page marked aria-current="page". With href every page is a real link, so it works with no JavaScript and through your router; with onPageChange the pages are buttons. Below 640px the numbers give way to "Page 3 of 12" between Previous and Next. Without pageCount, for a list that is not counted, it shows Previous and Next only, and hasNextPage enables Next.

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

In its source: app/routes/my-recipes.tsx · app/components/cook/RecipeCookView.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/pagination#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/pagination

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/pagination#v1.2.13 ourostack/ouro-ui/pagination-tests#v1.2.13
import { Pagination } from "@/components/ouro/pagination/pagination";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Pagination page={3} pageCount={12} href="/recipes?page={page}" />
<Pagination page={page} pageCount={12} onPageChange={setPage} />
<Pagination page={page} hasNextPage={rows.length > pageSize} href="?page={page}" />

Usage

Use Tabs to switch between a few named views.

Use instead when

  • Use ShowMore instead when the next rows should join the list in place.

Props

The props the modes share. Type: PaginationProps.

className
Type: string
Layout only: margins, width, grid placement (mt-6). Appearance classes warn in development.
label
Type: string
Default: "Pagination"
Names the navigation for screen readers.
nextLabel
Type: string
Default: "Next"
The text of the link to the page after.
page (required)
Type: number
The current page, counting from 1.
pageLabel
Type: string
Default: "Page {page}"
What a screen reader calls each page number; {page} is the number.
previousLabel
Type: string
Default: "Previous"
The text of the link to the page before.
ref
Type: Ref<HTMLElement>
Reaches the rendered element.
siblings
Type: number
Default: 1
How many pages show on each side of the current one before a gap ("1 … 4 5 6 … 12" is 1). The list keeps the same number of cells on every page, so Next never moves.
summaryLabel
Type: string
Default: "Page {page} of {pageCount}"
The text between Previous and Next below 640px, where the page numbers do not fit; {page} and {pageCount} are the numbers.

Modes

PaginationProps is a union: pass the props of exactly one mode. Mixing them is a type error.

With href and hasNextPage

hasNextPage
Type: boolean
Default: false
Whether there is a page after this one, which enables Next.
href (required)
Type: string | ((page: number) => string)
The address of a page: a string with {page} (href="/recipes?page={page}", which a Server Component can pass) or a function (href={(page) => /recipes?page=${page}}). Internal paths go through OuroProvider's linkComponent, so your router navigates.

Not in this mode (typed never):

  • onPageChange: Not with href. Remove href to render buttons that call onPageChange.
  • pageCount: Leave it out when the list has no count; pass it to show the page numbers.

With href and pageCount

href (required)
Type: string | ((page: number) => string)
The address of a page: a string with {page} (href="/recipes?page={page}", which a Server Component can pass) or a function (href={(page) => /recipes?page=${page}}). Internal paths go through OuroProvider's linkComponent, so your router navigates.
pageCount (required)
Type: number
How many pages there are. From a count of rows: Math.ceil(total / pageSize).

Not in this mode (typed never):

  • hasNextPage: Not with pageCount, which decides whether there is a next page.
  • onPageChange: Not with href. Remove href to render buttons that call onPageChange.

With onPageChange and hasNextPage

hasNextPage
Type: boolean
Default: false
Whether there is a page after this one, which enables Next.
onPageChange (required)
Type: (page: number) => void
Called with the page number when the person picks a page. Update page in it.

Not in this mode (typed never):

  • href: Not with onPageChange. Remove onPageChange to render links.
  • pageCount: Leave it out when the list has no count; pass it to show the page numbers.

With onPageChange and pageCount

onPageChange (required)
Type: (page: number) => void
Called with the page number when the person picks a page. Update page in it.
pageCount (required)
Type: number
How many pages there are. From a count of rows: Math.ceil(total / pageSize).

Not in this mode (typed never):

  • hasNextPage: Not with pageCount, which decides whether there is a next page.
  • href: Not with onPageChange. Remove onPageChange to render links.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • count: Not a prop. Ouro UI uses pageCount: <Pagination page={1} pageCount={12}>.
  • currentPage: Not a prop. Ouro UI uses page, counting from 1: <Pagination page={1} pageCount={12}>.
  • onChange: Not a prop. Ouro UI uses onPageChange, which gets the page number: onPageChange={(page) => …}.
  • total: Not a prop. Pass the number of pages: pageCount={Math.ceil(total / pageSize)}.
  • totalPages: Not a prop. Ouro UI uses pageCount: <Pagination page={1} pageCount={12}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.