Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

[Shadcn/ui React Series — Part 24] Pagination: Navigating Large Data the Right Way

4 min readFeb 2, 2026

--

Press enter or click to view image in full size

Shadcn/ui React Series Navigation Components:

Read documentation here

Demo in here

Pagination is a navigation component used to split large datasets into smaller, manageable chunks.
You’ll see it everywhere: tables, blog lists, search results, admin dashboards.

Without pagination, users are forced to scroll endlessly or wait for large payloads to load — both hurt usability and performance.

In this part, we’ll build a clean, accessible Pagination component, understand its building blocks, and explore real-world usage patterns in React applications.

When Should You Use Pagination?

Use pagination when:

  • Displaying large datasets (tables, logs, reports)
  • Fetching data from APIs with page-based queries
  • You want predictable navigation between pages
  • SEO matters (page-based URLs)

Avoid pagination when:

  • Infinite scroll provides better UX (feeds, timelines)
  • The dataset is very small
  • Users need quick scanning rather than structured navigation

Installation

Pagination in shadcn/ui is a headless component, not Radix-based.

No additional dependency is required.

File Structure

src/
└─ components/
└─ ui/
└─ pagination.tsx

Pagination Component

components/ui/pagination.tsx

Get Rivai Nasution’s stories in your inbox

Join Medium for free to get updates from this writer.

The Pagination component is composed of small, reusable primitives:

  • Pagination → navigation wrapper
  • PaginationContent → list container
  • PaginationItem → list item
  • PaginationLink → page link
  • PaginationPrevious / PaginationNext
  • PaginationEllipsis → collapsed pages indicator

This design keeps it flexible and easy to adapt.

import * as React from "react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";

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("gap-0.5 flex items-center", 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"> &
React.ComponentProps<"a">;

function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<Button
asChild
variant={isActive ? "outline" : "ghost"}
size={size}
className={cn(className)}
>
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
{...props}
/>
</Button>
);
}

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" className="cn-rtl-flip" />
<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" className="cn-rtl-flip" />
</PaginationLink>
);
}

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

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

Basic Usage

A simple pagination with previous, pages, and next buttons.

Press enter or click to view image in full size
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
1
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>

Pagination with Ellipsis

Use ellipsis when dealing with many pages.

Press enter or click to view image in full size
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">10</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>

This keeps the UI compact while still giving context.

Active Page Handling

The active state is controlled via the isActive prop.

<PaginationLink href="#" isActive>
3
</PaginationLink>

Under the hood:

  • aria-current="page" improves accessibility
  • Visual styles switch automatically using Button variants

Accesibility Notes

Pagination includes:

  • nav with aria-label="pagination"
  • aria-current for active pages
  • Screen-reader-only labels for ellipsis

Best practices:

  • Always show current page
  • Ensure clickable targets are large enough
  • Combine pagination with server-side data fetching when possible

Pagination vs Infinite Scroll

Pagination

  • Better for SEO
  • Predictable navigation
  • Easier state management

Infinite Scroll

  • Better for discovery
  • Continuous browsing
  • Harder to reach the footer

Choose based on user intent, not trends.

Best Practices

  • Keep pagination centered and visible
  • Avoid too many page numbers
  • Use ellipsis for large ranges
  • Sync pagination state with URL params
  • Disable Previous/Next when needed

Summary

  • Pagination helps manage large datasets cleanly
  • Built from composable primitives
  • Accessible by default
  • Easy to integrate with tables, lists, and APIs
  • A must-have for dashboards and data-heavy apps

--

--