Pagination React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React pagination for tables and lists: numbered pages with ellipses, previous / next, page size, jump to page and load more.
Click a page number and the current-page pill slides to it on a quick spring; previous and next step one page and grey out at the ends, the optional « » buttons jump to the first and last page. With the keyboard, Tab reaches every control, and while focus is on the pages Left / Right go to the previous / next page and keep focus on the new current page, Home / End jump to the first / last. Type a number in "Go to" and press Enter to jump (it clamps to the real range). The page size select keeps the first row you were looking at on screen. The summary ("21 to 40 of 318") is announced once per change. When it is narrower than 640px (a phone, a card, a side panel) it becomes ‹ Page 5 of 16 › with the summary under the text: tap it for a list of every page (a bottom sheet on phones); buttons are 44px on touch. The load-more variant shows how many are left and a thin progress bar, and with autoLoad it loads on its own when you scroll within 200px of it.
Pagination is a Pro React component in the React navigation components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add pagination, together with Button, Field, Select, useControllableState, useLocale and 6 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add paginationPagination is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Pagination from @/components/ui/pagination.
import { useState } from "react"
import { Pagination } from "@/components/ui/pagination"
export function InvoicesFooter({ total }: { total: number }) {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
return (
<Pagination
total={total}
page={page}
onPageChange={setPage}
pageSize={pageSize}
onPageSizeChange={setPageSize}
showPageSize
locale="en-US"
labels={{
summary: (a, b, t) => `${a}\u2013${b} of ${t} invoices`,
pageSize: "Invoices per page",
}}
/>
)
}Props
pagenumberThe current page, 1-based, controlled. A page beyond pageCount renders as given with next disabled; it is never corrected silently.
defaultPagedefault1numberThe starting page while uncontrolled.
onPageChange(page: number) => voidFires on every page change: a click, an arrow key, the jump field, the compact page picker or a page size change.
pageCountnumberTotal pages. Default: ceil(total / pageSize). 0 renders only the summary ("No results").
totalnumberThe item count, for the page count and the summary ("1 to 20 of 318"). Without it the summary reads "Page 5 of 16".
pageSizenumberItems per page, controlled.
defaultPageSizedefault"20"numberItems per page while uncontrolled; also used when total is given without pageSize.
onPageSizeChange(pageSize: number) => voidFires when the page size select changes. The page moves so the first visible item stays on screen: page = floor(firstIndex / newSize) + 1.
pageSizeOptionsdefault"[10, 20, 50, 100]"number[]The choices in the page size select.
variantdefault"numbers""numbers" | "simple" | "compact" | "load-more"numbers: ‹ 1 … 4 5 6 … 20 ›. simple: Previous / Next buttons with the summary between. compact: ‹ Page 5 of 20 › where the text opens a page picker. load-more: a full-width button for feeds with a remaining count and a progress bar.
mobileVariantdefault"compact""compact" | "simple" | "numbers"What renders while the pagination is narrower than 640px: a phone, or a card or side panel on desktop (a CSS container query). Both layouts are in the server HTML and CSS picks one, so nothing jumps after hydration. numbers drops the sibling pages there so it fits 360px.
siblingCountdefault1numberPages shown on each side of the current one. The slots come from getPageRange(page, pageCount, siblingCount, boundaryCount), exported for custom UIs; their count stays constant while the page moves.
boundaryCountdefault1numberPages always shown at each end.
showFirstLastdefaultfalsebooleanAdds « and » buttons that jump to the first and last page. Below 640px the numbers row drops them (its boundary pages already reach the ends) so it fits 360px.
showSummarydefaulttruebooleanShows "1 to 20 of 318" (a polite live region, so screen readers hear each change once).
showPageSizedefaultfalsebooleanShows the page size select ("20 per page"); a bottom sheet on phones.
showJumpdefaultfalsebooleanShows a small "Go to" number field: Enter jumps, out-of-range numbers clamp. Hidden on phones when the compact picker is there.
aligndefault"between""start" | "center" | "end" | "between"between puts the summary at the start and the pages at the end; the others keep them together.
sizedefault"md""sm" | "md" | "lg"Button height 32 / 40 / 48px (md is 44px on touch). Page buttons are square and grow for long numbers.
activeVariantdefault"soft""soft" | "solid" | "outline"The current page's pill: a soft tint (like tabs), the solid tone fill, or an outline.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape of every button, scaled with the size like the button's; full makes round page pills.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The current page's color (and the load-more button and bar). Neutral is black in light mode and white in dark mode.
colordefault""stringAny CSS color for the current page; replaces the tone. Leave empty to use the tone.
loadingdefaultfalsebooleanThe next page is loading: the control you pressed shows a spinner and the rest are disabled until it turns false.
disableddefaultfalsebooleanDims and blocks every control.
autoLoaddefaultfalsebooleanload-more only: loads the next batch on its own when the button scrolls within 200px (IntersectionObserver in the nearest scroll container).
localedefault""stringNumber formatting ("1.250" in de-DE, "1,250" in en-US). Empty uses the LocaleProvider, then the page language.
getPageHref(page: number) => stringRenders every page as a link (NavLink through the app's router link) instead of a button: crawlable, open in a new tab, router-friendly. Arrow keys, the jump field and the page picker navigate too.
linkComponent(props: LinkComponentProps) => ReactNodeThe link component for this instance; default the LinkProvider's, else a plain <a>.
hasNextPagebooleanCursor pagination without a known count: when set and neither total nor pageCount is, only previous / next render, next disabled when false.
hasMorebooleanload-more: whether there is more to load. Default loadedCount < total. false shows the end message.
onLoadMore(() => void | Promise<unknown>)load-more: called by the button (or autoLoad). Return a promise to show the loading state until it settles.
loadedCountnumberload-more: items already on screen, for "Showing 40 of 318", the remaining count and the bar.
labelsPartial<PaginationLabels>Every string, English by default: navigation, previous, next, first, last, page(n), pageOf(page, count), summary(from, to, total), empty, pageSize, perPage(size), jump, loadMore(count), remaining(count), loading, end, showing(shown, total). Numbers arrive formatted for the locale. DEFAULT_PAGINATION_LABELS is exported to spread and override.
classNamesPartial<Record<"meta" | "summary" | "controls" | "list" | "item" | "link" | "ellipsis" | "indicator" | "previous" | "next" | "first" | "last" | "size" | "jump" | "compact" | "loadMore" | "loadMoreStatus" | "loadMoreBar", string>>Extra classes for the inner parts.
Parts
getPageRange
functiongetPageRange(page, pageCount, siblingCount = 1, boundaryCount = 1): the visible slots, like [1, "start-ellipsis", 4, 5, 6, "end-ellipsis", 20], for building your own pager.
DEFAULT_PAGINATION_LABELS
constantThe English strings: previous, next, first, last, the summary, page size, jump, load more and the screen reader page names. Override keys through labels.
PaginationRangeItem
typeOne slot from getPageRange: a page number, "start-ellipsis" or "end-ellipsis".
PaginationProps
typeThe props of Pagination, for wrappers that connect it to your router or data hook.
Examples
Four variants
numbers, simple, compact and load-more, the same data.
Admin table footer
Page size, jump to page and first / last buttons. Changing the size keeps your first row on screen.
Pages as links
getPageHref renders real links through the router: crawlable and fine in a new tab. The address bar follows.
Load more in a feed
autoLoad loads before you reach the end; the bar and the remaining count follow.
Cursor pagination
No total, just hasNextPage: only previous and next, with a spinner on the one you pressed.
Sizes, radius and color
32, 40 and 48px, a brand solid pill and an outlined custom color.
ro-RO and en-US
The same 24,980 results formatted for each locale.
Dependencies
Files
- components/ui/pagination.tsx
FAQ
Is the React Pagination component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Pagination?
Run npx wingo-ui@latest add pagination. It also adds the Wingo UI items it builds on (Button, Field, Select, useControllableState, useLocale and 6 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/pagination.
How does Pagination behave on phones?
When it is narrower than 640px (a phone, a card, a side panel) it becomes ‹ Page 5 of 16 › with the summary under the text: tap it for a list of every page (a bottom sheet on phones); buttons are 44px on touch.
See also
Used in
More React navigation components
Articles using this component
- TanStack Table v9 Data Table: Filters, Bulk Actions, Mobile
Component guides12 min read