VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Navigation
  3. Pagination

Pagination

Page navigator with collapsing ellipses.

NavigationClient component
……

Page 5 of 24

Install

bash
npx @agents-npm-packages/vibeui add pagination --dir ./src/components/ui

Import

tsx
import { Pagination } from "@/components/ui/pagination"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<Pagination>

PropTypeDefaultDescription
page*number—Current page, 1-based.
totalPages*number—Total number of pages.
onPageChange*(page: number) => void—Called with the new page number.
classNamestring—Extra classes, merged with the component's own.

Source

src/components/ui/pagination.tsx
"use client";

import { CaretLeft, CaretRight } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";

type PageItem = number | `gap-${number}`;

function range(page: number, total: number): PageItem[] {
  const pages = new Set([1, total, page - 1, page, page + 1]);
  const sorted = [...pages]
    .filter((p) => p >= 1 && p <= total)
    .sort((a, b) => a - b);
  const out: PageItem[] = [];
  sorted.forEach((p, i) => {
    const prev = sorted[i - 1];
    if (prev !== undefined && p - prev > 1) {
      // A single missing page is shown instead of an ellipsis.
      out.push(p - prev === 2 ? prev + 1 : `gap-${prev}`);
    }
    out.push(p);
  });
  return out;
}

/** Page navigator with collapsing ellipses. */
export function Pagination({
  page,
  totalPages,
  onPageChange,
  className,
}: {
  page: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  className?: string;
}) {
  const cell =
    "flex size-8 items-center justify-center rounded-sm text-[13px] font-medium tabular-nums transition-[background-color,color,transform] duration-150 active:scale-90";

  return (
    <nav aria-label="Pagination" className={cn("flex items-center gap-1", className)}>
      <button
        type="button"
        className={cn(cell, "text-muted hover:bg-surface-muted hover:text-ink disabled:pointer-events-none disabled:opacity-35")}
        disabled={page <= 1}
        onClick={() => onPageChange(page - 1)}
        aria-label="Previous page"
      >
        <CaretLeft size={14} weight="bold" />
      </button>
      {range(page, totalPages).map((p) =>
        typeof p === "string" ? (
          <span key={p} aria-hidden className="w-6 text-center text-xs text-faint">
            …
          </span>
        ) : (
          <button
            key={p}
            type="button"
            aria-current={p === page ? "page" : undefined}
            onClick={() => onPageChange(p)}
            className={cn(
              cell,
              p === page
                ? "bg-ink text-surface shadow-quiet"
                : "text-muted hover:bg-surface-muted hover:text-ink",
            )}
          >
            {p}
          </button>
        ),
      )}
      <button
        type="button"
        className={cn(cell, "text-muted hover:bg-surface-muted hover:text-ink disabled:pointer-events-none disabled:opacity-35")}
        disabled={page >= totalPages}
        onClick={() => onPageChange(page + 1)}
        aria-label="Next page"
      >
        <CaretRight size={14} weight="bold" />
      </button>
    </nav>
  );
}

Exports Pagination. Peer dependencies: clsx, tailwind-merge, @phosphor-icons/react.

PreviousFloating NavNextSegmented