suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Pagination

Navigation links for moving between pages of content, such as table results.

Pagination is an open-source component from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

import * as React from "react"
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  PaginationSummary,
} from "@/components/ui/pagination"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function pageRange(page: number, count: number) {
  if (count <= 7) return Array.from({ length: count }, (_, index) => index + 1)
  const start = Math.max(1, Math.min(page - 2, count - 6))
  const pages: (number | "ellipsis")[] = Array.from(
    { length: 5 },
    (_, index) => start + index
  )
  if (pages.at(-1) !== count) pages.push("ellipsis", count)
  return pages
}

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function NumberedExample() {
  const [page, setPage] = React.useState(2)
  const perPage = 5
  const count = Math.ceil(orders.length / perPage)

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <PaginationSummary>
          {(page - 1) * perPage + 1}–{Math.min(page * perPage, orders.length)} /{" "}
          {orders.length}
        </PaginationSummary>
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious
              text=""
              {...go(setPage, page - 1, page === 1)}
            />
          </PaginationItem>
          {pageRange(page, count).map((entry, index) =>
            entry === "ellipsis" ? (
              <PaginationItem key={`ellipsis-${index}`}>
                <PaginationEllipsis />
              </PaginationItem>
            ) : (
              <PaginationItem key={entry}>
                <PaginationLink
                  isActive={entry === page}
                  {...go(setPage, entry, false)}
                >
                  {entry}
                </PaginationLink>
              </PaginationItem>
            )
          )}
          <PaginationItem>
            <PaginationNext
              text=""
              {...go(setPage, page + 1, page === count)}
            />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </Orders>
  )
}

export default function PaginationDemo() {
  return <NumberedExample />
}

Pagination is a set of link primitives you compose around your own page state. Pagination is the nav landmark. Inside it, PaginationContent is a list of PaginationItem entries, each holding a PaginationLink, PaginationPrevious, PaginationNext, PaginationFirst, PaginationLast or PaginationEllipsis.

For compact toolbars, PaginationGroup joins arrow links into one segmented control, PaginationInput lets the user type a page number, and PaginationSummary shows a range such as 6–10 / 62. The component does not calculate page ranges; you decide which links to render.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/pagination

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add pagination

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @phosphor-icons/react class-variance-authority
  1. Add the pieces it builds on: Button.

  2. Copy the source into components/ui/pagination.tsx:

components/ui/pagination.tsx
"use client"

import {
  CaretDoubleLeftIcon,
  CaretDoubleRightIcon,
  CaretLeftIcon,
  CaretRightIcon,
  DotsThreeIcon,
} from "@phosphor-icons/react"
import type { VariantProps } from "class-variance-authority"
import type * as React from "react"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"

function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  return (
  1. Update the import paths to match your project.

Usage

import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationFirst, PaginationLast, PaginationEllipsis, PaginationSummary, PaginationGroup, PaginationInput } from "@/components/ui/pagination"
<NumberedExample />

Links render as a elements, so pass href for real routes, or href="#" with an onClick that calls preventDefault() for client-side state. Mark the current page with isActive on PaginationLink. To disable an edge link, set aria-disabled on it; it becomes non-interactive and dimmed.

PaginationLink takes a size from the Button sizes and defaults to icon-sm. Set text="" on PaginationPrevious or PaginationNext for icon-only arrows.

Examples

Compact

A page number input with joined previous and next arrows in a PaginationGroup, clamping typed values to the valid range.

import * as React from "react"
import {
  Pagination,
  PaginationGroup,
  PaginationInput,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function CompactExample() {
  const [page, setPage] = React.useState(3)
  const [draft, setDraft] = React.useState("3")
  const perPage = 5
  const count = Math.ceil(orders.length / perPage)

  const change = (next: number) => {
    setPage(next)
    setDraft(String(next))
  }

  const commit = () => {
    const next = Number.parseInt(draft, 10)
    if (Number.isNaN(next)) return setDraft(String(page))
    change(Math.min(Math.max(next, 1), count))
  }

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <div className="flex items-center gap-2 text-sm text-muted-foreground">
          <label htmlFor="orders-page">Page</label>
          <PaginationInput
            id="orders-page"
            value={draft}
            onChange={(event) => setDraft(event.target.value)}
            onBlur={commit}
            onKeyDown={(event) => event.key === "Enter" && commit()}
          />
          <span className="tabular-nums">/ {count}</span>
        </div>
        <PaginationGroup>
          <PaginationPrevious text="" {...go(change, page - 1, page === 1)} />
          <PaginationNext text="" {...go(change, page + 1, page === count)} />
        </PaginationGroup>
      </Pagination>
    </Orders>
  )
}

export default function PaginationCompact() {
  return <CompactExample />
}

Rows per page

A rows-per-page select, a range summary, and first, previous, next and last arrows.

import * as React from "react"
import {
  Pagination,
  PaginationFirst,
  PaginationGroup,
  PaginationLast,
  PaginationNext,
  PaginationPrevious,
  PaginationSummary,
} from "@/components/ui/pagination"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function RowsPerPageExample() {
  const [page, setPage] = React.useState(1)
  const [perPage, setPerPage] = React.useState(5)
  const count = Math.ceil(orders.length / perPage)

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <Select
          value={String(perPage)}
          onValueChange={(value) => {
            setPerPage(Number(value))
            setPage(1)
          }}
        >
          <SelectTrigger
            size="sm"
            className="h-8 text-sm"
            aria-label="Rows per page"
          >
            <SelectValue>{(value: string) => `${value} rows`}</SelectValue>
          </SelectTrigger>
          <SelectContent side="top">
            {[5, 10, 20].map((value) => (
              <SelectItem key={value} value={String(value)}>
                {value} rows
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <PaginationSummary>
          {(page - 1) * perPage + 1}–{Math.min(page * perPage, orders.length)} /{" "}
          {orders.length}
        </PaginationSummary>
        <PaginationGroup>
          <PaginationFirst {...go(setPage, 1, page === 1)} />
          <PaginationPrevious text="" {...go(setPage, page - 1, page === 1)} />
          <PaginationNext text="" {...go(setPage, page + 1, page === count)} />
          <PaginationLast {...go(setPage, count, page === count)} />
        </PaginationGroup>
      </Pagination>
    </Orders>
  )
}

export default function PaginationRowsPerPage() {
  return <RowsPerPageExample />
}

With ellipsis

A long page range shortened with ellipses on both sides of the current page.

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationWithEllipsis() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">9</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            10
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">11</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">24</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Custom labels

Previous and next links relabelled Newer and Older.

import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationCustomLabels() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" text="Newer" />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" text="Older" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Large links

Page links using the larger icon button size.

import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
} from "@/components/ui/pagination"

export default function PaginationLargeLinks() {
  return (
    <Pagination>
      <PaginationContent>
        {[1, 2, 3, 4, 5].map((page) => (
          <PaginationItem key={page}>
            <PaginationLink href="#" size="icon" isActive={page === 1}>
              {page}
            </PaginationLink>
          </PaginationItem>
        ))}
      </PaginationContent>
    </Pagination>
  )
}

API Reference

Anatomy7 parts
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious />
<PaginationLink />
<PaginationEllipsis />
<PaginationNext />
</PaginationItem>
</PaginationContent>
</Pagination>
01

Pagination

The nav landmark that contains the pagination controls.

Also accepts the props of React.ComponentProps<"nav">.

02

PaginationContent

The list that holds the pagination items.

Also accepts the props of React.ComponentProps<"ul">.

03

PaginationItem

A list item wrapper for one link or ellipsis.

Also accepts the props of React.ComponentProps<"li">.

04

PaginationLink

A page link styled as a button, highlighted when active.

PropTypeDefaultDescription
isActiveboolean-Marks the link as the current page with aria-current="page".

Also accepts the props of PaginationLinkProps.

05

PaginationPrevious

A link to the previous page with a leading caret.

PropTypeDefaultDescription
textstring"Previous"The visible label. A custom value also becomes the accessible name; an empty string renders an icon-only link.
isActiveboolean-Marks the link as the current page with aria-current="page".

Also accepts the props of React.ComponentProps<typeof PaginationLink>.

06

PaginationNext

A link to the next page with a trailing caret.

PropTypeDefaultDescription
textstring"Next"The visible label. A custom value also becomes the accessible name; an empty string renders an icon-only link.
isActiveboolean-Marks the link as the current page with aria-current="page".

Also accepts the props of React.ComponentProps<typeof PaginationLink>.

07

PaginationFirst

An icon link to the first page.

PropTypeDefaultDescription
isActiveboolean-Marks the link as the current page with aria-current="page".

Also accepts the props of React.ComponentProps<typeof PaginationLink>.

08

PaginationLast

An icon link to the last page.

PropTypeDefaultDescription
isActiveboolean-Marks the link as the current page with aria-current="page".

Also accepts the props of React.ComponentProps<typeof PaginationLink>.

09

PaginationEllipsis

A placeholder that indicates skipped pages.

Also accepts the props of React.ComponentProps<"span">.

10

PaginationSummary

Secondary text that summarizes the current range.

Also accepts the props of React.ComponentProps<"p">.

11

PaginationGroup

Joins several links into one segmented control.

Also accepts the props of React.ComponentProps<"div">.

12

PaginationInput

A numeric text field for jumping to a page.

Also accepts the props of React.ComponentProps<"input">.

Accessibility

  • Pagination renders a nav with aria-label="pagination".
  • The active PaginationLink gets aria-current="page".
  • Arrow links have built-in accessible names: Go to previous page, Go to next page, Go to first page and Go to last page. Custom text on previous or next replaces the name.
  • PaginationEllipsis is hidden from assistive technology.
  • PaginationGroup has role="group".
  • PaginationInput has no built-in label; associate one with htmlFor or aria-label.

Notes

  • Styling hooks: data-slot values pagination, pagination-content, pagination-item, pagination-link, pagination-ellipsis, pagination-summary, pagination-group and pagination-input. Active links expose data-active.
  • Active links use the secondary button variant and others use ghost.
  • Previous and next text is hidden below the sm breakpoint, and the carets flip in right-to-left layouts.
  • PaginationInput uses inputMode="numeric" to show a number keypad on touch devices.
NextPin PadPreviousPage Indicator

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Compact
    • Rows per page
    • With ellipsis
    • Custom labels
    • Large links
  4. 04API Reference
    • Pagination
    • PaginationContent
    • PaginationItem
    • PaginationLink
    • PaginationPrevious
    • PaginationNext
    • PaginationFirst
    • PaginationLast
    • PaginationEllipsis
    • PaginationSummary
    • PaginationGroup
    • PaginationInput
  5. 05Accessibility
  6. 06Notes