# Pagination

Page numbers with smart gaps and a pill that glides to the current page.

**Category:** Navigation  
**Exports:** `Pagination`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/pagination.json
```
The file lands at `components/ui/pagination.tsx`; import from `@/components/ui/pagination`.

## API — Pagination
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `page / defaultPage` | `number` | — |  |
| `onPageChange` | `(page: number) => void` | — |  |
| `total` | `number` | — |  |
| `siblings` | `number` | `1` |  |

## Example — Pagination

```tsx
import * as React from "react";
import { Pagination } from "@/components/ui/pagination";

export default function PaginationDemo({ total = 12, siblings = 1 }: { total?: number; siblings?: number }) {
  const [page, setPage] = React.useState(4);
  return (
    <div className="flex flex-col items-center gap-4">
      <Pagination page={page} onPageChange={setPage} total={total} siblings={siblings} />
      <p className="text-sm text-muted-foreground">
        Showing results {(page - 1) * 20 + 1}–{page * 20} of 240
      </p>
    </div>
  );
}
```

## Source — components/ui/pagination.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type PaginationProps = {
  page?: number;
  defaultPage?: number;
  onPageChange?: (page: number) => void;
  total: number;
  /** Pages shown on each side of the current page. */
  siblings?: number;
  className?: string;
};

function range(page: number, total: number, siblings: number): (number | "gap")[] {
  const first = Math.max(2, page - siblings);
  const last = Math.min(total - 1, page + siblings);
  const out: (number | "gap")[] = [1];
  if (first > 2) out.push("gap");
  for (let p = first; p <= last; p++) out.push(p);
  if (last < total - 1) out.push("gap");
  if (total > 1) out.push(total);
  return out;
}

export function Pagination({ page: pageProp, defaultPage = 1, onPageChange, total, siblings = 1, className }: PaginationProps) {
  const id = React.useId();
  const reduce = useReducedMotion();
  const [inner, setInner] = React.useState(defaultPage);
  const page = pageProp ?? inner;

  const go = (p: number) => {
    const next = Math.min(total, Math.max(1, p));
    if (next === page) return;
    if (pageProp === undefined) setInner(next);
    onPageChange?.(next);
  };

  const arrow = (dir: -1 | 1) => (
    <button
      type="button"
      onClick={() => go(page + dir)}
      disabled={dir < 0 ? page <= 1 : page >= total}
      aria-label={dir < 0 ? "Previous page" : "Next page"}
      className="grid size-9 cursor-pointer place-items-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-35"
    >
      <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
        <path d={dir < 0 ? "m15 6-6 6 6 6" : "m9 6 6 6-6 6"} />
      </svg>
    </button>
  );

  return (
    <nav aria-label="Pagination" className={cn("flex items-center gap-1", className)}>
      {arrow(-1)}
      <ul className="flex items-center gap-1">
        {range(page, total, siblings).map((p, i) =>
          p === "gap" ? (
            <li key={`gap-${i}`} className="grid size-9 place-items-center text-sm text-muted-foreground" aria-hidden>
              …
            </li>
          ) : (
            <li key={p}>
              <button
                type="button"
                onClick={() => go(p)}
                aria-current={p === page ? "page" : undefined}
                aria-label={`Page ${p}`}
                className={cn(
                  "relative grid h-9 min-w-9 cursor-pointer place-items-center rounded-lg px-2 text-sm tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                  p === page ? "font-medium text-background" : "text-muted-foreground hover:bg-accent hover:text-foreground",
                )}
              >
                {p === page && (
                  <motion.span
                    layoutId={`${id}-current`}
                    className="absolute inset-0 rounded-lg bg-foreground"
                    transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 500, damping: 36 }}
                  />
                )}
                <span className="relative z-10">{p}</span>
              </button>
            </li>
          ),
        )}
      </ul>
      {arrow(1)}
    </nav>
  );
}
```