# 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](/pricing/#nextjs).

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](/nextjs/) 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.

```html
<nav aria-label="Orders pages">
  <ul class="pagination">
    <li class="page-item disabled">
      <button type="button" class="page-link" disabled>Previous</button>
    </li>
    <li class="page-item active" aria-current="page">
      <button type="button" class="page-link">1</button>
    </li>
    <li class="page-item">
      <button type="button" class="page-link">2</button>
    </li>
    <li class="page-item">
      <button type="button" class="page-link">3</button>
    </li>
    <li class="page-item">
      <button type="button" class="page-link">Next</button>
    </li>
  </ul>
</nav>
```
```tsx code title="src/components/pagination.tsx"
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:

```tsx title="src/components/orders-table.tsx"
"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:

```tsx title="src/app/(dashboard)/orders/page.tsx"
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](/nextjs/docs/components/tables/) for the table markup. When people also need to search and sort, use the [Data table](/nextjs/docs/plugins/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:

```html
<nav aria-label="Invoices pages">
  <ul class="pagination pagination-sm justify-content-end">
    <li class="page-item active" aria-current="page">
      <button type="button" class="page-link">1</button>
    </li>
    <li class="page-item">
      <button type="button" class="page-link">2</button>
    </li>
    <li class="page-item">
      <button type="button" class="page-link">3</button>
    </li>
  </ul>
</nav>
```
```tsx code
<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 an `aria-label` that 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"`. The `active` class only changes its color.
- Set `disabled` on the button of a disabled item, so Tab skips it and a click does nothing. On an `<a>`, the `disabled` class does not stop the keyboard: add `tabIndex={-1}` and `aria-disabled="true"`.
- Previous and Next buttons that show only an icon need an `aria-label`, as in the demo.

## Related

- [React Bootstrap pagination](/react/docs/navigation/pagination/): the same markup in AdminKit for React (Vite)
- [Bootstrap 5 pagination](/bootstrap/docs/navigation/pagination/): the pagination of the HTML version
- [Tables](/nextjs/docs/components/tables/): the table markup to page through
- [Data table](/nextjs/docs/plugins/data-table/): search, sorting and a built-in pager
- [General UI demo](https://nextjs-demo.adminkit.io/ui-general): pagination in three sizes, breadcrumbs and other Bootstrap components

---

Canonical HTML page: https://adminkit.io/nextjs/docs/navigation/pagination/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
