# Shadcn Pagination

> Free shadcn/ui pagination for React — previous/next, numbered links, and an ellipsis. With the page-range truncation logic, a controlled pager, a compact variant, a data-table footer, and SEO-friendly links.

Source: https://designrevision.com/components/pagination

The **Shadcn Pagination** is a set of composable controls for paging through results — previous/next, numbered links, and an ellipsis. It is **presentational**: it renders the buttons (styled with `buttonVariants`, no dependency), and **you** supply the page logic. The examples below include that logic — range truncation, a controlled pager, a table footer, and SEO-friendly links.

## Anatomy

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem><PaginationPrevious href="#" /></PaginationItem>
    <PaginationItem><PaginationLink href="#">1</PaginationLink></PaginationItem>
    <PaginationItem><PaginationLink href="#" isActive>2</PaginationLink></PaginationItem>
    <PaginationItem><PaginationEllipsis /></PaginationItem>
    <PaginationItem><PaginationNext href="#" /></PaginationItem>
  </PaginationContent>
</Pagination>
```

`PaginationLink` takes `isActive` (which sets `aria-current="page"`), and `PaginationPrevious` / `PaginationNext` / `PaginationEllipsis` are styled helpers.

## Truncating with an ellipsis

For large totals you don't render every page — show the first, the last, a window around the current page, and ellipses for the gaps:

```tsx
function getPages(current, total) {
  const pages = [1]
  if (current > 3) pages.push("ellipsis")
  for (let p = Math.max(2, current - 1); p <= Math.min(total - 1, current + 1); p++) pages.push(p)
  if (current < total - 2) pages.push("ellipsis")
  if (total > 1) pages.push(total)
  return pages
}
```

The Truncated example renders exactly this.

## Controlled (client-side)

Track the page in state and update on click, disabling the ends:

```tsx
const [page, setPage] = React.useState(1)

<PaginationPrevious
  href="#"
  onClick={(e) => { e.preventDefault(); setPage((p) => Math.max(1, p - 1)) }}
  className={cn(page === 1 && "pointer-events-none opacity-50")}
/>
```

## Server pagination & SEO

For crawlable pages, use **real links** with a page query so each page is its own URL and the server renders the right slice:

```tsx
<PaginationLink href={`?page=${n}`} isActive={n === current}>{n}</PaginationLink>
```

`PaginationLink` already sets `aria-current="page"` on the active item. The SEO links example uses this.

## In a data table

Tables usually want a compact bar — a **rows-per-page** Select with first / previous / next / last icon buttons and a "Page X of Y" label. The Table footer example is that bar, ready to wire to your table state (or TanStack Table's pagination API).

## Accessibility

`Pagination` is a `<nav aria-label="pagination">`, the active page carries `aria-current="page"`, and the ellipsis is hidden from assistive tech (`aria-hidden`) with screen-reader text. Use real `href`s where you can so pages work without JavaScript.

## Installation

### pagination

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination
```

**Dependencies:** utils, button

**Usage & accessibility:** Page navigation — a <Pagination> nav wrapping <PaginationContent> (a list) of <PaginationItem>s holding <PaginationLink isActive>, plus <PaginationPrevious>, <PaginationNext>, and <PaginationEllipsis>. Links are styled with buttonVariants (no dependency). It is presentational: you compute which page numbers to show and wire isActive / href (or onClick) yourself — the examples include the range + ellipsis logic.

```tsx
// components/ui/pagination.tsx
import * as React from "react"
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  MoreHorizontalIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"

function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  return (
    <nav
      role="navigation"
      aria-label="pagination"
      data-slot="pagination"
      className={cn("mx-auto flex w-full justify-center", className)}
      {...props}
    />
  )
}

function PaginationContent({
  className,
  ...props
}: React.ComponentProps<"ul">) {
  return (
    <ul
      data-slot="pagination-content"
      className={cn("flex flex-row items-center gap-1", className)}
      {...props}
    />
  )
}

function PaginationItem({ ...props }: React.ComponentProps<"li">) {
  return <li data-slot="pagination-item" {...props} />
}

type PaginationLinkProps = {
  isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
  React.ComponentProps<"a">

function PaginationLink({
  className,
  isActive,
  size = "icon",
  ...props
}: PaginationLinkProps) {
  return (
    <a
      aria-current={isActive ? "page" : undefined}
      data-slot="pagination-link"
      data-active={isActive}
      className={cn(
        buttonVariants({
          variant: isActive ? "outline" : "ghost",
          size,
        }),
        className
      )}
      {...props}
    />
  )
}

function PaginationPrevious({
  className,
  ...props
}: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink
      aria-label="Go to previous page"
      size="default"
      className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
      {...props}
    >
      <ChevronLeftIcon />
      <span className="hidden sm:block">Previous</span>
    </PaginationLink>
  )
}

function PaginationNext({
  className,
  ...props
}: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink
      aria-label="Go to next page"
      size="default"
      className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
      {...props}
    >
      <span className="hidden sm:block">Next</span>
      <ChevronRightIcon />
    </PaginationLink>
  )
}

function PaginationEllipsis({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      aria-hidden
      data-slot="pagination-ellipsis"
      className={cn("flex size-9 items-center justify-center", className)}
      {...props}
    >
      <MoreHorizontalIcon className="size-4" />
      <span className="sr-only">More pages</span>
    </span>
  )
}

export {
  Pagination,
  PaginationContent,
  PaginationLink,
  PaginationItem,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
}
```

## Examples

### Basic

The canonical pagination — previous/next with numbered links and an ellipsis.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-demo-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-demo-01.tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationDemo01() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

---

### Truncated with ellipsis

Smart range truncation for many pages — a `getPages` helper inserts leading/trailing ellipses.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-ellipsis-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-ellipsis-01.tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

// Compute the page list with leading/trailing ellipses for a large total.
function getPages(current: number, total: number) {
  const pages: (number | "ellipsis")[] = [1]
  if (current > 3) pages.push("ellipsis")
  for (
    let p = Math.max(2, current - 1);
    p <= Math.min(total - 1, current + 1);
    p++
  ) {
    pages.push(p)
  }
  if (current < total - 2) pages.push("ellipsis")
  if (total > 1) pages.push(total)
  return pages
}

export default function PaginationEllipsis01() {
  const current = 6
  const total = 20
  const pages = getPages(current, total)

  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        {pages.map((page, i) => (
          <PaginationItem key={i}>
            {page === "ellipsis" ? (
              <PaginationEllipsis />
            ) : (
              <PaginationLink href="#" isActive={page === current}>
                {page}
              </PaginationLink>
            )}
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

---

### Controlled

A working pager driven by `useState`, with prev/next disabled at the bounds.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-controlled-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-controlled-01.tsx
"use client"

import * as React from "react"

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

export default function PaginationControlled01() {
  const total = 5
  const [page, setPage] = React.useState(1)

  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious
            href="#"
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => Math.max(1, p - 1))
            }}
            className={cn(page === 1 && "pointer-events-none opacity-50")}
          />
        </PaginationItem>
        {Array.from({ length: total }).map((_, i) => (
          <PaginationItem key={i}>
            <PaginationLink
              href="#"
              isActive={page === i + 1}
              onClick={(e) => {
                e.preventDefault()
                setPage(i + 1)
              }}
            >
              {i + 1}
            </PaginationLink>
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext
            href="#"
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => Math.min(total, p + 1))
            }}
            className={cn(page === total && "pointer-events-none opacity-50")}
          />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

---

### Compact

A minimal pager — just Previous / Page X of Y / Next.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-compact-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-compact-01.tsx
"use client"

import * as React from "react"

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

export default function PaginationCompact01() {
  const total = 10
  const [page, setPage] = React.useState(1)

  return (
    <Pagination>
      <PaginationContent className="gap-3">
        <PaginationItem>
          <PaginationPrevious
            href="#"
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => Math.max(1, p - 1))
            }}
            className={cn(page === 1 && "pointer-events-none opacity-50")}
          />
        </PaginationItem>
        <PaginationItem>
          <span className="text-muted-foreground px-2 text-sm">
            Page {page} of {total}
          </span>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext
            href="#"
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => Math.min(total, p + 1))
            }}
            className={cn(page === total && "pointer-events-none opacity-50")}
          />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

---

### Table footer

The data-table bar — a rows-per-page Select with first/prev/next/last icon buttons.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-table-01
```

**Dependencies:** lucide-react, button, @designrevision/select

```tsx
// components/ui/pagination-table-01.tsx
"use client"

import * as React from "react"
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
} from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export default function PaginationTable01() {
  const totalRows = 97
  const [pageSize, setPageSize] = React.useState("10")
  const [page, setPage] = React.useState(1)
  const totalPages = Math.ceil(totalRows / Number(pageSize))

  return (
    <div className="flex w-full max-w-2xl flex-col items-center gap-4 sm:flex-row sm:justify-between">
      <div className="flex items-center gap-2 text-sm">
        <span className="text-muted-foreground">Rows per page</span>
        <Select
          value={pageSize}
          onValueChange={(v) => {
            setPageSize(v)
            setPage(1)
          }}
        >
          <SelectTrigger className="h-8 w-[70px]">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {["10", "20", "50"].map((s) => (
              <SelectItem key={s} value={s}>
                {s}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>
      <div className="flex items-center gap-4 text-sm">
        <span className="text-muted-foreground">
          Page {page} of {totalPages}
        </span>
        <div className="flex items-center gap-1">
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            disabled={page === 1}
            onClick={() => setPage(1)}
          >
            <ChevronsLeft />
            <span className="sr-only">First page</span>
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            disabled={page === 1}
            onClick={() => setPage((p) => p - 1)}
          >
            <ChevronLeft />
            <span className="sr-only">Previous page</span>
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            disabled={page === totalPages}
            onClick={() => setPage((p) => p + 1)}
          >
            <ChevronRight />
            <span className="sr-only">Next page</span>
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            disabled={page === totalPages}
            onClick={() => setPage(totalPages)}
          >
            <ChevronsRight />
            <span className="sr-only">Last page</span>
          </Button>
        </div>
      </div>
    </div>
  )
}
```

---

### SEO links

Real anchor links (`?page=N`) for server-rendered, crawlable pagination.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-links-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-links-01.tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

// Real anchor links (?page=N) for server-rendered, SEO-friendly pagination.
export default function PaginationLinks01() {
  const current = 2
  const total = 5

  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href={`?page=${current - 1}`} />
        </PaginationItem>
        {Array.from({ length: total }).map((_, i) => (
          <PaginationItem key={i}>
            <PaginationLink href={`?page=${i + 1}`} isActive={current === i + 1}>
              {i + 1}
            </PaginationLink>
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext href={`?page=${current + 1}`} />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

---

### With results count

A search-results footer — a "Showing X–Y of Z" summary beside a pager.

**Install:**

```bash
npx shadcn@latest add @designrevision/pagination-results-01
```

**Dependencies:** pagination

```tsx
// components/ui/pagination-results-01.tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationResults01() {
  const current = 2
  const pageSize = 10
  const totalItems = 97
  const start = (current - 1) * pageSize + 1
  const end = Math.min(current * pageSize, totalItems)

  return (
    <div className="flex w-full max-w-2xl flex-col items-center gap-3 sm:flex-row sm:justify-between">
      <p className="text-muted-foreground text-sm">
        Showing {start}–{end} of {totalItems} results
      </p>
      <Pagination className="mx-0 w-auto justify-end">
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious href="#" />
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#">1</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#" isActive>
              2
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#">3</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationEllipsis />
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#">10</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationNext href="#" />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </div>
  )
}
```
