Pagination

Controlled page navigation component.

API Reference

PropTypeDefault
pagenumber
totalPagesnumber
onPageChange(page: number) => void
color"root" | "primary""root"

Preview

<Pagination page={page} totalPages={10} onPageChange={setPage} />

Basic Usage

import { Pagination } from "@/ui";

<Pagination page={1} totalPages={10} onPageChange={setPage} />

Source

src/ui/Pagination.tsx
import { cn } from "@/utils";
import { IconButton } from "./IconButton";
import { ArrowBackIcon, ArrowForwardIcon } from "./icons";

interface PaginationProps {
  page: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  color?: "root" | "primary";
}

function getPageRange(page: number, totalPages: number): (number | "...")[] {
  if (totalPages <= 7) {
    return Array.from({ length: totalPages }, (_, i) => i + 1);
  }
  const pages: (number | "...")[] = [1];
  if (page > 3) pages.push("...");
  for (
    let i = Math.max(2, page - 1);
    i <= Math.min(totalPages - 1, page + 1);
    i++
  ) {
    pages.push(i);
  }
  if (page < totalPages - 2) pages.push("...");
  pages.push(totalPages);
  return pages;
}

export function Pagination({
  page,
  totalPages,
  onPageChange,
  color = "root",
}: PaginationProps) {
  if (totalPages <= 1) return null;

  const range = getPageRange(page, totalPages);

  return (
    <nav
      aria-label="Pagination"
      className="flex items-center justify-center gap-1"
    >
      <IconButton
        variant="minimal"
        color={color}
        aria-label="Previous page"
        onClick={() => onPageChange(page - 1)}
        disabled={page === 1}
      >
        <ArrowBackIcon />
      </IconButton>

      {range.map((p, i) =>
        p === "..." ? (
          <span
            key={`ellipsis-${i}`}
            className="px-1 text-sm text-root-contrast-subtle"
          >

          </span>
        ) : (
          <button
            key={p}
            onClick={() => onPageChange(p)}
            aria-label={`Page ${p}`}
            aria-current={p === page ? "page" : undefined}
            className={cn(
              "inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1",
              p === page
                ? "pointer-events-none cursor-default bg-root-contrast font-semibold text-root"
                : "text-root-contrast-subtle hover:bg-root-100 hover:text-root-contrast",
            )}
          >
            {p}
          </button>
        ),
      )}

      <IconButton
        variant="minimal"
        color={color}
        aria-label="Next page"
        onClick={() => onPageChange(page + 1)}
        disabled={page === totalPages}
      >
        <ArrowForwardIcon />
      </IconButton>
    </nav>
  );
}