Pagination

Free · MIT

Outline, soft, ghost, pill, neo, minimal and carousel-dot pagination with a sliding active marker or press feedback, tones, sizes and shapes, plus first/last, range info, page-size select and jump-to-page.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/pagination.json

The file lands in components/goodui/pagination.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/pagination works too.

Usage

import * as React from "react"
import { Pagination } from "@/components/goodui/pagination"

export function InvoicesFooter({ total }: { total: number }) {
  const [page, setPage] = React.useState(1)
  const [pageSize, setPageSize] = React.useState(25)

  return (
    <Pagination
      aria-label="Invoices pages"
      page={page}
      onPageChange={setPage}
      totalItems={total}
      pageSize={pageSize}
      onPageSizeChange={setPageSize}
      pageSizeOptions={[25, 50, 100]}
      showInfo
      showPageSize
      showLabels
    />
  )
}

When to use

Use it for
  • Paging through a table of bookings, invoices or clients, with "21–30 of 248" and a rows-per-page select.
  • Search or list pages where people jump to a page number: showJump.
  • Compact "Page 3 of 12" controls in a card footer or a mobile view: variant="minimal".
  • Dot indicators under a carousel or onboarding slides: variant="dots".
Reach for something else
  • Feeds where people just keep reading: load more on scroll or with a button instead of pages.
  • A multi-step form: use stepper.
  • Switching between a few named views: use tabs.
If you needUse
Data tablesvariant="outline" (default) with totalItems, showInfo and showPageSize
Quiet pagination under a listvariant="ghost" or "soft"
Pages sitting in one rounded trackvariant="pill"
Small spaces and mobilevariant="minimal" ("Page 3 of 12"), showJump makes the number editable
Carousels and slidesvariant="dots"
Very long result setsshowFirstLast and showJump; siblings={2} shows more neighbours
Playful or retro brandsvariant="neo" animation="press"

Examples

Minimal with editable page number

<Pagination variant="minimal" totalPages={12} defaultPage={3} showJump size="sm" />

Carousel dots

<Pagination variant="dots" totalPages={slides.length} page={slide} onPageChange={setSlide} aria-label="Testimonials" />

Translated

<Pagination
  totalItems={248}
  showInfo
  labels={{
    previousPage: "Vorherige Seite",
    nextPage: "Nächste Seite",
    goToPage: (p) => `Seite ${p}`,
    range: (from, to, total) => `${from}–${to} von ${total}`,
  }}
/>

API reference

PropTypeDefault
page
Current page, 1-based (controlled). Clamped to 1…total.
number—
defaultPage
Initial page when uncontrolled.
number1
onPageChange
Called with the new 1-based page.
(page: number) => void—
totalPages
Number of pages. Ignored when `totalItems` is set.
number1
totalItems
Total rows; with `pageSize` it sets the page count and the "21–30 of 248" info.
number—
pageSize
Rows per page (controlled).
number—
defaultPageSize
Initial rows per page when uncontrolled.
number10
onPageSizeChange
Called when the rows select changes; the page is adjusted to keep the first visible row.
(size: number) => void—
pageSizeOptions
Choices in the rows-per-page select.
number[][10, 25, 50, 100]
siblings
Pages shown on each side of the current one.
number1
boundaries
Pages always shown at the start and end.
number1
variant
outline (default), soft, ghost, pill (pages in a track), neo, minimal ("Page 3 of 12") or dots (carousels).
"outline" | "soft" | "ghost" | "pill" | "neo" | "minimal" | "dots""outline"
tone
Color of the active page marker and focus rings.
"primary" | "neutral" | "success" | "warning" | "danger" | "info""primary"
animation
slide glides the active marker between pages; press shrinks buttons while pressed.
"none" | "slide" | "press""slide"
size"sm" | "md" | "lg""md"
shape
Button corners. The pill variant is always round.
"rounded" | "circle" | "square""rounded"
showFirstLast
Adds first-page and last-page buttons.
booleanfalse
showPrevNext
Shows the previous and next arrows.
booleantrue
showLabels
"Previous"/"Next" text next to the arrows (hidden on phones).
booleanfalse
showInfo
"21–30 of 248" (with totalItems) or "Page 3 of 12".
booleanfalse
showPageSize
Adds a rows-per-page select.
booleanfalse
showJump
A "Go to" input. In the minimal variant the page number itself becomes editable.
booleanfalse
labels
Text for built-in controls and screen readers; pass translations here.
Partial<PaginationLabels>—

Also accepts every <nav> attribute.

Accessibility

Enter / SpaceActivates the focused page, arrow or "…" button (native <button>).
EnterIn the jump-to field, goes to the typed page (clamped to 1…total). In the minimal variant leaving the field also applies it.
TabMoves through the controls; focus rings show on keyboard focus only.
  • A <nav> with aria-label "Pagination" (override with aria-label, recommended when a page has more than one).
  • The current page button has aria-current="page" ("true" on dots). Every button has a name: "Page 4", "Next page", "Forward 3 pages", "Slide 2 of 5", all in labels.
  • Under dir="rtl" the previous/next, first/last and skip icons are mirrored so they point the way the page list reads.
  • Disabled arrows on the first and last page are real disabled buttons.
  • The "21–30 of 248" text and the minimal "Page 3 of 12" sit in aria-live="polite" regions, so page changes are announced.
  • The rows select is a native <select> inside a <label>; the jump field is a numeric input with aria-label "Go to page".

Motion

  • slide (default): the active marker glides to the new page as a shared layout element on a spring (stiffness 520, damping 40); on dots, the wide active dot travels and the window shifts.
  • press: buttons shrink to 0.9 while pressed (neo pushes into its shadow). Colors change in 150 ms.
  • The "…" buttons swap to a double arrow on hover or focus, showing they skip several pages.
  • Reduced motion: the marker jumps, transitions are off and there is no press effect.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • Pages are 1-based. Use page + onPageChange (controlled) or defaultPage; the page is clamped to 1…total.
  • Pass totalItems (with pageSize) for tables: it computes the page count and the range info. totalPages is ignored when totalItems is set.
  • Changing page size keeps the first visible row on screen, so onPageChange can fire together with onPageSizeChange; handle both.
  • "…" buttons are clickable and skip max(3, siblings × 2 + 3) pages. The page list keeps a constant length, so the control does not jump in width.
  • Every built-in string is in labels (previous, nextPage, goToPage, range, pageOf, rows, goTo…); functions take numbers. The nav name comes from aria-label.

FAQ

Can I use it with server-side paging?

Yes. Keep page in the URL or state, fetch on onPageChange and pass totalItems from your API's count. The component never slices data itself.

Is paginationRange exported?

Yes. paginationRange(page, total, siblings, boundaries) returns the numbers and "start-ellipsis" / "end-ellipsis" markers, if you want the same logic in your own markup.

Related