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.