For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Pagination

The Pagination component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/pagination

Source

import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

import { Button } from "@/components/ui/button";

const PAGINATION_DOTS = "dots";

function range(start: number, end: number) {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}

/**
 * The page numbers to show: first, last, the current page and `siblingCount`
 * neighbours on each side, with "dots" where pages are skipped. Changing the
 * page re-windows the list, so the active cell moves as you click.
 */
function paginationRange(
  current: number,
  total: number,
  siblingCount = 1
): (number | typeof PAGINATION_DOTS)[] {
  if (siblingCount * 2 + 5 >= total) {
    return range(1, total);
  }
  const left = Math.max(current - siblingCount, 1);
  const right = Math.min(current + siblingCount, total);
  const showLeftDots = left > 2;
  const showRightDots = right < total - 2;
  if (!showLeftDots && showRightDots) {
    return [...range(1, 3 + 2 * siblingCount), PAGINATION_DOTS, total];
  }
  if (showLeftDots && !showRightDots) {
    return [
      1,
      PAGINATION_DOTS,
      ...range(total - (2 + 2 * siblingCount), total),
    ];
  }
  return [1, PAGINATION_DOTS, ...range(left, right), PAGINATION_DOTS, total];
}

function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  return (
    <nav
      aria-label="pagination"
      data-slot="pagination"
      className={cn(
        "mx-auto flex w-full items-center justify-between gap-2",
        className
      )}
      {...props}
    />
  );
}

function PaginationContent({
  className,
  ...props
}: React.ComponentProps<"ul">) {
  return (
    <ul
      data-slot="pagination-content"
      className={cn(
        // Previous and Next sit at the edges, the page numbers in between.
        "flex w-full min-w-0 items-center justify-center gap-0.5 [&>li:has([data-edge=next])]:ml-auto [&>li:has([data-edge=previous])]:mr-auto",
        className
      )}
      {...props}
    />
  );
}

function PaginationItem({ ...props }: React.ComponentProps<"li">) {
  return <li data-slot="pagination-item" {...props} />;
}

type PaginationLinkProps = {
  isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size" | "variant"> &
  React.ComponentProps<"a">;

function PaginationLink({
  className,
  children,
  isActive,
  size = "icon",
  variant,
  ...props
}: PaginationLinkProps) {
  // Page cells have no transition, so the old number does not fade on change.
  // A link with its own variant (Previous, Next) keeps the button feel.
  const cell = variant === undefined;
  return (
    <Button
      variant={variant ?? (isActive ? "outline" : "ghost")}
      size={size}
      className={cn(
        "rounded-[8px] text-sm font-medium",
        cell && "[transition:none] active:not-aria-[haspopup]:scale-100",
        cell && size === "icon" && "size-8",
        cell &&
          (isActive
            ? "border-border bg-popover text-foreground shadow-[shadow:var(--shadow-control,0_1px_2px_0_rgb(0_0_0/0.05))] hover:bg-popover"
            : "text-[color:var(--foreground-secondary,var(--muted-foreground))] hover:bg-[var(--surface-strong,var(--muted))] hover:text-foreground dark:hover:bg-[var(--surface-strong,var(--muted))]"),
        className
      )}
      nativeButton={false}
      render={
        <a
          aria-current={isActive ? "page" : undefined}
          data-slot="pagination-link"
          data-active={isActive}
          {...props}
        >
          {children}
        </a>
      }
    />
  );
}

function PaginationPrevious({
  className,
  text = "Previous",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label="Go to previous page"
      data-edge="previous"
      size="sm"
      variant="outline"
      className={cn("rounded-[8px]", className)}
      {...props}
    >
      <RiArrowLeftLine data-icon="inline-start" />
      <span className="hidden sm:block">{text}</span>
    </PaginationLink>
  );
}

function PaginationNext({
  className,
  text = "Next",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label="Go to next page"
      data-edge="next"
      size="sm"
      variant="outline"
      className={cn("rounded-[8px]", className)}
      {...props}
    >
      <span className="hidden sm:block">{text}</span>
      <RiArrowRightLine data-icon="inline-end" />
    </PaginationLink>
  );
}

function PaginationEllipsis({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      aria-hidden
      data-slot="pagination-ellipsis"
      className={cn(
        "flex size-8 items-center justify-center rounded-[8px] text-sm font-medium text-[color:var(--foreground-subtle,var(--muted-foreground))]",
        className
      )}
      {...props}
    >
      <span aria-hidden>…</span>
      <span className="sr-only">More pages</span>
    </span>
  );
}

export {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  PAGINATION_DOTS,
  paginationRange,
};