Pagination

Page numbers with smart ellipses and a sliding active pill.

Showing results 41–50 of 200

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/pagination.tsx

    components/ui/pagination.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { ChevronLeft, ChevronRight } from "lucide-react";
    import { useId, useState } from "react";
    
    /** Returns page numbers with "…" gaps, e.g. [1, "…", 4, 5, 6, "…", 20] */
    export function pageRange(current: number, total: number, siblings = 1): (number | "…")[] {
      const range: (number | "…")[] = [];
      const start = Math.max(2, current - siblings);
      const end = Math.min(total - 1, current + siblings);
      range.push(1);
      if (start > 2) range.push("…");
      for (let i = start; i <= end; i++) range.push(i);
      if (end < total - 1) range.push("…");
      if (total > 1) range.push(total);
      return range;
    }
    
    export function Pagination({ page, total, onChange }: { page: number; total: number; onChange: (page: number) => void }) {
      const pillId = useId();
      const btn = "grid h-9 min-w-9 place-items-center rounded-lg px-2 text-sm transition-colors disabled:pointer-events-none disabled:opacity-40";
    
      return (
        <nav aria-label="Pagination" className="flex items-center gap-1">
          <button className={`${btn} text-zinc-600 hover:bg-zinc-100`} onClick={() => onChange(page - 1)} disabled={page === 1} aria-label="Previous page">
            <ChevronLeft className="size-4" />
          </button>
          {pageRange(page, total).map((p, i) =>
            p === "…" ? (
              <span key={`gap-${i}`} className="px-1 text-sm text-zinc-400">
                …
              </span>
            ) : (
              <button key={p} onClick={() => onChange(p)} aria-current={p === page ? "page" : undefined} className={`${btn} relative ${p === page ? "text-white" : "text-zinc-600 hover:bg-zinc-100"}`}>
                {p === page && <motion.span layoutId={pillId} className="absolute inset-0 rounded-lg bg-zinc-900" transition={{ type: "spring", stiffness: 450, damping: 34 }} />}
                <span className="relative tabular-nums">{p}</span>
              </button>
            ),
          )}
          <button className={`${btn} text-zinc-600 hover:bg-zinc-100`} onClick={() => onChange(page + 1)} disabled={page === total} aria-label="Next page">
            <ChevronRight className="size-4" />
          </button>
        </nav>
      );
    }
    
    export default function Demo() {
      const [page, setPage] = useState(5);
      return (
        <div className="flex flex-col items-center gap-3">
          <Pagination page={page} total={20} onChange={setPage} />
          <p className="text-sm text-zinc-500">Showing results {(page - 1) * 10 + 1}–{page * 10} of 200</p>
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.