Next.js Bootstrap Pagination
Next.js Bootstrap pagination for the App Router: a pager in a client component that pages a table, with rows passed in from a server component page.
This component is only available in AdminKit PRO. Learn more.
Pagination breaks a long list into pages: a table of orders, search results, a grid of cards. People see which page they are on, jump to another by its number, or step through with Previous and Next.
AdminKit for Next.js has no pagination component of its own. You write Bootstrap's markup in JSX, keep the current page in React state and mark that page active. Pagination is part of AdminKit PRO. The stylesheet of the free version leaves out Bootstrap's pagination styles.
In the App Router, pages are server components: they render on the server and have no state. A pager has to remember the current page and react to clicks, so it lives in a client component, a file that starts with "use client". The page itself stays a server component. It can still pass the rows to the client component, because rows are plain data.
Example
This Pagination component only draws the pager. Its parent keeps the current page and passes it in, with a function to call when the user picks another page. Previous is disabled on the first page and Next on the last.
type PaginationProps = {
page: number;
pageCount: number;
onPageChange: (page: number) => void;
label: string;
};
export function Pagination({ page, pageCount, onPageChange, label }: PaginationProps) {
const pages = Array.from({ length: pageCount }, (_, index) => index + 1);
const isFirst = page === 1;
const isLast = page === pageCount;
return (
<nav aria-label={label}>
<ul className="pagination">
<li className={`page-item${isFirst ? " disabled" : ""}`}>
<button
type="button"
className="page-link"
disabled={isFirst}
onClick={() => onPageChange(page - 1)}
>
Previous
</button>
</li>
{pages.map((number) => (
<li
key={number}
className={`page-item${number === page ? " active" : ""}`}
aria-current={number === page ? "page" : undefined}
>
<button type="button" className="page-link" onClick={() => onPageChange(number)}>
{number}
</button>
</li>
))}
<li className={`page-item${isLast ? " disabled" : ""}`}>
<button
type="button"
className="page-link"
disabled={isLast}
onClick={() => onPageChange(page + 1)}
>
Next
</button>
</li>
</ul>
</nav>
);
}This file has no "use client" of its own. It takes a function, onPageChange, and a server component cannot pass a function to the browser. So only client components render it, and it runs in the browser with them. The items are buttons because they change state on the same page. When every page has its own URL, use Link from next/link with the page-link class instead.
Classes
| Class | Element | Effect |
|---|---|---|
pagination | <ul> | Joins the items into one row of connected boxes |
page-item | <li> | One item |
page-link | <button> or <a> | The box people click |
active | .page-item | Marks the current page in the primary color; add aria-current="page" too |
disabled | .page-item | Greys out an item and ignores clicks; also set disabled on the button |
pagination-sm, pagination-lg | .pagination | Smaller or larger items |
The pager in AdminKit is a little more compact than Bootstrap's. src/scss/1-variables/_app.scss sets the item padding to 0.3rem 0.75rem, or 0.15rem 0.6rem small and 0.35rem 1rem large. The items have the background of cards in light and dark mode. Their text is --bs-gray-600, and --bs-gray-800 on hover. src/scss/3-components/_pagination.scss keeps the number of the active page white.
Pagination with a table
Most of the time, a pager sits below a table that shows one page of rows. The table component holds the current page and cuts that page out of the rows with slice. It starts with "use client" because it has state:
"use client";
import { useState } from "react";
import { Pagination } from "@/components/pagination";
export type Order = { id: string; customer: string; total: string };
const pageSize = 10;
export function OrdersTable({ orders }: { orders: Order[] }) {
const [page, setPage] = useState(1);
const pageCount = Math.max(1, Math.ceil(orders.length / pageSize));
const rows = orders.slice((page - 1) * pageSize, page * pageSize);
return (
<div className="card">
<div className="card-body">
<table className="table table-striped">
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th className="text-end">Total</th>
</tr>
</thead>
<tbody>
{rows.map((order) => (
<tr key={order.id}>
<td>{order.id}</td>
<td>{order.customer}</td>
<td className="text-end">{order.total}</td>
</tr>
))}
</tbody>
</table>
<Pagination
page={page}
pageCount={pageCount}
onPageChange={setPage}
label="Orders pages"
/>
</div>
</div>
);
}The page stays a server component and passes the rows as a prop. The static HTML that Next.js builds already shows the first page of rows. In the browser, the table takes over and the pager starts to respond to clicks:
import { OrdersTable, type Order } from "@/components/orders-table";
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/orders");
const orders: Order[] = [
{ id: "#1042", customer: "Ava Brooks", total: "$240.00" },
{ id: "#1041", customer: "Liam Carter", total: "$89.50" },
// ...
];
export default function OrdersPage() {
return (
<>
<h1 className="h3 mb-3">Orders</h1>
<OrdersTable orders={orders} />
</>
);
}pageMetadata() reads the route's title and description from src/data/pages.json, so add new routes there. See Tables for the table markup. When people also need to search and sort, use the Data table plugin. It has a pager of its own, drawn with the same pagination, page-item and page-link classes, so both pagers look alike.
Sizes and alignment
A smaller pager suits a busy card. Add pagination-sm or pagination-lg to the list, and move the pager with flexbox utilities such as justify-content-center or justify-content-end. Without state or handlers, this markup works in a server component too:
<nav aria-label="Invoices pages">
<ul className="pagination pagination-sm justify-content-end">
<li className="page-item active" aria-current="page">
<button type="button" className="page-link">1</button>
</li>
<li className="page-item">
<button type="button" className="page-link">2</button>
</li>
<li className="page-item">
<button type="button" className="page-link">3</button>
</li>
</ul>
</nav>Accessibility
- Wrap the list in a
<nav>with anaria-labelthat names what it pages through, such as "Orders pages". A page often has more than one navigation region. - Give the current page
aria-current="page". Theactiveclass only changes its color. - Set
disabledon the button of a disabled item, so Tab skips it and a click does nothing. On an<a>, thedisabledclass does not stop the keyboard: addtabIndex={-1}andaria-disabled="true". - Previous and Next buttons that show only an icon need an
aria-label, as in the demo.
Related
- React Bootstrap pagination: the same markup in AdminKit for React (Vite)
- Bootstrap 5 pagination: the pagination of the HTML version
- Tables: the table markup to page through
- Data table: search, sorting and a built-in pager
- General UI demo: pagination in three sizes, breadcrumbs and other Bootstrap components
Last updated on
BreadcrumbPro
Add a Next.js Bootstrap breadcrumb to App Router pages: static in a server component, or built from usePathname with next/link in a client component.
Form control
Next.js Bootstrap form controls: inputs and textareas as Bootstrap markup in server components, with state and submits handled in client components.