rapidUI

Pagination

Page navigation with previous/next, page links and ellipsis.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/pagination.json

Installs to @/components/ui/pagination.

Manual installation

Install the dependencies:

pnpm add lucide-react@^1.48.0

Add the components it builds on: button.

Copy the source into your project:

components/ui/pagination.tsx
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
import type * as React from 'react'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

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

function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
	return (
		<ul
			data-slot="pagination-content"
			className={cn('group/pagination flex items-center gap-0.5', className)}
			{...props}
		/>
	)
}

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

type PaginationLinkProps = {
	isActive?: boolean
	/** Total page count. When set, small screens show only the active link as "current / total" and hide the other page links and ellipses. */
	total?: number
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
	React.ComponentProps<'a'>

function PaginationLink({ className, isActive, size = 'icon', total, children, ...props }: PaginationLinkProps) {
	const compact = total !== undefined
	const showTotal = isActive && compact
	return (
		<Button
			variant={isActive ? 'outline' : 'ghost'}
			size={size}
			className={cn(compact && !isActive && 'max-sm:hidden', showTotal && 'max-sm:w-auto max-sm:px-3', className)}
			nativeButton={false}
			render={
				<a
					aria-current={isActive ? 'page' : undefined}
					data-slot="pagination-link"
					data-active={isActive}
					data-compact={compact || undefined}
					{...props}
				>
					{children}
					{showTotal && <span className="ms-1 sm:hidden">/ {total}</span>}
				</a>
			}
		/>
	)
}

function PaginationPrevious({
	className,
	text = 'Previous',
	...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
	return (
		<PaginationLink aria-label="Go to previous page" size="default" className={cn('pl-1.5!', className)} {...props}>
			<ChevronLeftIcon 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" size="default" className={cn('pr-1.5!', className)} {...props}>
			<span className="hidden sm:block">{text}</span>
			<ChevronRightIcon data-icon="inline-end" />
		</PaginationLink>
	)
}

function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
	return (
		<span
			aria-hidden
			data-slot="pagination-ellipsis"
			className={cn(
				"flex size-10 items-center justify-center max-sm:group-has-data-compact/pagination:hidden [&_svg:not([class*='size-'])]:size-4",
				className,
			)}
			{...props}
		>
			<MoreHorizontalIcon />
			<span className="sr-only">More pages</span>
		</span>
	)
}

export {
	Pagination,
	PaginationContent,
	PaginationEllipsis,
	PaginationItem,
	PaginationLink,
	PaginationNext,
	PaginationPrevious,
}

A pagination component that helps users navigate through multiple pages of content. Built with accessibility in mind, it provides a consistent way to handle page navigation in lists, tables, or any content that spans multiple pages.

Features

  • Accessible <nav> landmark with aria-label="pagination"
  • Page links rendered as anchors (<a>) styled with Button, so they work with href and client routers
  • Active page highlighting (isActive renders the outline button variant and sets aria-current="page")
  • Previous/Next links with a chevron icon; the label is set with the text prop and hidden below the sm breakpoint
  • Ellipsis for long page ranges
  • Button icon sizes for page links (icon-xs, icon-sm, icon, icon-lg)
  • Centered by default (justify-center); override with className

Usage

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from '@/components/ui/pagination'

export function BasicPagination() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Controlled

PaginationLink renders an anchor, so prevent the default navigation when handling clicks in state. There is no disabled prop; mark an unavailable control with aria-disabled and disable pointer events.

export function ControlledPagination() {
  const [page, setPage] = React.useState(1)
  const isFirst = page === 1

  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious
            href="#"
            aria-disabled={isFirst}
            tabIndex={isFirst ? -1 : undefined}
            className={isFirst ? 'pointer-events-none opacity-50' : undefined}
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => Math.max(1, p - 1))
            }}
          />
        </PaginationItem>
        {/* Page numbers */}
        <PaginationItem>
          <PaginationNext
            href="#"
            onClick={(e) => {
              e.preventDefault()
              setPage((p) => p + 1)
            }}
          />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Custom Labels

<PaginationPrevious href="#" text="Earlier meeting" />
<PaginationNext href="#" text="Later meeting" />

Examples

Default

Basic pagination with a few pages and navigation controls.

With More Pages

Pagination with ellipsis for handling many pages.

Custom Text

Previous and Next labels set with the text prop, here stepping between council meetings.

Sizes

Page links accept the Button size prop. They default to icon.

With Disabled Controls

Pagination with a disabled Previous control.

Controlled

A public diary paged in state, with a sliding window of page numbers and ellipses.

A compact footer for a case list: a result summary, rows per page and icon-only first/previous/next/last links with aria-labels.

Subcomponents

Pagination

The root container component that wraps the pagination controls.

PaginationContent

A container for pagination items, providing proper layout and spacing.

PaginationItem

A wrapper for individual pagination elements.

An anchor for page numbers, styled as a Button. Accepts all a props plus:

  • isActive - Marks the current page (outline variant, aria-current="page")
  • size - Button size, defaults to icon

PaginationPrevious

A link for navigating to the previous page. Accepts PaginationLink props plus text (default "Previous").

PaginationNext

A link for navigating to the next page. Accepts PaginationLink props plus text (default "Next").

PaginationEllipsis

An indicator for skipped page numbers.

Accessibility

The Pagination component follows WAI-ARIA guidelines:

  • Uses semantic HTML elements
  • Provides proper ARIA labels
  • Supports keyboard navigation
  • Indicates current page
  • Communicates disabled states
  • Maintains focus management
  • Screen reader friendly

Best Practices

  1. Navigation:

    • Keep consistent page ordering
    • Provide clear visual feedback
    • Handle edge cases (first/last page)
    • Consider mobile interactions
  2. Layout:

    • Maintain proper spacing
    • Ensure touch targets are large enough
    • Use clear visual hierarchy
    • Consider responsive behavior
  3. Interaction:

    • Handle loading states
    • Provide immediate feedback
    • Maintain URL synchronization
    • Consider infinite scroll alternatives
  4. Accessibility:

    • Use descriptive labels
    • Support keyboard shortcuts
    • Maintain focus states
    • Consider screen readers
  5. Implementation:

    • Handle state changes gracefully
    • Consider performance implications
    • Implement proper error handling
    • Maintain URL structure
  • Table
  • Data Table
  • Button