Skip to content
All Taste components

Navigation

Pagination

Moves through long lists a page at a time, with numbered pages or a compact range.

numbered · ellipsis · compact with rows per page · disabled ends

import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";

/** Pages to show: first, last, and one either side of the current page. */
function pageList(page: number, total: number): (number | "gap")[] {
  const out: (number | "gap")[] = [];
  for (let p = 1; p <= total; p++) {
    if (p === 1 || p === total || Math.abs(p - page) <= 1) out.push(p);
    else if (out[out.length - 1] !== "gap") out.push("gap");
  }
  return out;
}

const item =
  "inline-flex h-8 min-w-8 items-center justify-center gap-1 rounded border border-transparent px-2 text-[13px] font-medium tabular-nums text-fg-muted transition hover:bg-surface-2 hover:text-fg [&_svg]:size-3.5";

export function Pagination({ page, total, onPage }: { page: number; total: number; onPage: (p: number) => void }) {
  return (
    <nav aria-label="Pagination">
      <ul className="flex flex-wrap items-center gap-1">
        <li>
          <button type="button" className={cn(item, "disabled:pointer-events-none disabled:opacity-45")} disabled={page === 1}
            aria-label="Previous page" onClick={() => onPage(page - 1)}>
            <ChevronLeft aria-hidden />Prev
          </button>
        </li>
        {pageList(page, total).map((p, i) =>
          p === "gap" ? (
            <li key={"gap" + i} aria-hidden className="w-6 text-center text-fg-subtle">…</li>
          ) : (
            <li key={p}>
              <button type="button" aria-current={p === page ? "page" : undefined} onClick={() => onPage(p)}
                className={cn(item, "aria-[current=page]:border-border-strong aria-[current=page]:bg-bg aria-[current=page]:text-fg")}>
                {p}
              </button>
            </li>
          ),
        )}
        <li>
          <button type="button" className={cn(item, "disabled:pointer-events-none disabled:opacity-45")} disabled={page === total}
            aria-label="Next page" onClick={() => onPage(page + 1)}>
            Next<ChevronRight aria-hidden />
          </button>
        </li>
      </ul>
    </nav>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.