Pagination
A controlled pager that reuses the secondary small Button for Previous / Next and renders 32px page cells. When there are many pages it collapses to leading and trailing dots around a sibling window, as in the example below.
Installation
You can add this pagination component using our CLI or manually:
- Install the following dependencies
npm install @remixicon/react - Create apagination.tsxfile and paste the following into it.
pagination.tsx
// components/base/pagination/pagination.tsx"use client"; import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";import { Button } from "@/components/base/buttons/button";import { cx } from "@/utils/cx"; // Controlled pager. Previous / Next reuse the secondary small Button; page// numbers are 32x32 cells and collapse to leading / trailing dots around a// sibling window when there are many pages. export interface PaginationProps { page: number; totalPages: number; onChange: (page: number) => void; /** Page numbers shown on each side of the current page. Default 1. */ siblingCount?: number; className?: string;} const DOTS = "dots"; function range(start: number, end: number): number[] { return Array.from({ length: end - start + 1 }, (_, i) => start + i);} function paginationRange(current, total, sibling) { const totalPageNumbers = sibling * 2 + 5; if (totalPageNumbers >= total) return range(1, total); const leftSibling = Math.max(current - sibling, 1); const rightSibling = Math.min(current + sibling, total); const showLeftDots = leftSibling > 2; const showRightDots = rightSibling < total - 2; if (!showLeftDots && showRightDots) { return [...range(1, 3 + 2 * sibling), DOTS, total]; } if (showLeftDots && !showRightDots) { return [1, DOTS, ...range(total - (2 + 2 * sibling), total)]; } return [1, DOTS, ...range(leftSibling, rightSibling), DOTS, total];} const cell = "flex size-8 shrink-0 items-center justify-center rounded-lg text-body-medium transition-[background-color,box-shadow,color] duration-150 ease"; export function Pagination({ page, totalPages, onChange, siblingCount = 1, className }: PaginationProps) { if (totalPages <= 1) return null; const pages = paginationRange(page, totalPages, siblingCount); return ( <nav aria-label="Pagination" className={cx("flex w-full items-center justify-between gap-2", className)}> <Button variant="secondary" size="small" leadingIcon={RiArrowLeftLine} disabled={page <= 1} onClick={() => onChange(page - 1)}> Previous </Button> <ul className="flex items-center gap-0.5"> {pages.map((item, i) => item === DOTS ? ( <li key={`dots-${i}`} aria-hidden className={cx(cell, "text-text-tertiary")}> … </li> ) : ( <li key={item}> <button type="button" aria-label={`Go to page ${item}`} aria-current={item === page ? "page" : undefined} onClick={() => onChange(item)} className={cx( cell, "cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-focus-ring", item === page ? "border border-border-button-default bg-background-primary-default text-text-primary shadow-xs" : "text-text-secondary hover:bg-background-secondary-hover hover:text-text-primary", )} > {item} </button> </li> ), )} </ul> <Button variant="secondary" size="small" trailingIcon={RiArrowRightLine} disabled={page >= totalPages} onClick={() => onChange(page + 1)}> Next </Button> </nav> );} - Update the import paths to match your project setup.
Table of Contents
Pagination is a controlled component — you own the current page state and it renders the page window, overflow dots and Previous / Next controls (which reuse the Buttoncomponent). We'll cover the following topics:
- Basic pagination
- Controlled page state
- Overflow & the sibling window
- Reusing the Button component