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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then 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.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/paginationIts 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.13import { 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
ShowMoreinstead 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 throughOuroProvider'slinkComponent, so your router navigates.
Not in this mode (typed never):
onPageChange: Not withhref. Removehrefto render buttons that callonPageChange.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 throughOuroProvider'slinkComponent, 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 withpageCount, which decides whether there is a next page.onPageChange: Not withhref. Removehrefto render buttons that callonPageChange.
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
pagein it.
Not in this mode (typed never):
href: Not withonPageChange. RemoveonPageChangeto 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
pagein 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 withpageCount, which decides whether there is a next page.href: Not withonPageChange. RemoveonPageChangeto 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 usespageCount:<Pagination page={1} pageCount={12}>.currentPage: Not a prop. Ouro UI usespage, counting from 1:<Pagination page={1} pageCount={12}>.onChange: Not a prop. Ouro UI usesonPageChange, 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 usespageCount:<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.