What is Pagination?
Pagination: Pagination splits a long set of results into numbered pages with controls to move between them, so each view loads a fixed, predictable amount.
Pagination divides a result set into pages of a fixed size, 25 or 50 rows for example, and gives the user controls to move between them: previous, next, and often numbered pages. It keeps each request and each screen small, gives every position in the list a stable address, and tells the user how much there is. Search results, admin tables, order histories and blog archives all rely on it.
Offset or cursor
- Offset pagination asks for
LIMIT 25 OFFSET 50. It supports jumping to page 7 and showing a total page count, but gets slow on deep pages and shows duplicates or skips rows when data is inserted while the user pages. - Cursor pagination asks for the 25 rows after a given ID or timestamp. It is fast and stable for changing data, but only supports previous and next, not arbitrary jumps.
- Load more appends the next page to the current list. It sits between pagination and infinite scroll.
Accessibility
Wrap the controls in a nav element with aria-label="Pagination" so it is a named landmark, and put the page links in a list. Mark the current page with aria-current="page". Visible labels such as "3" are fine; you can add an accessible name like "Page 3" for clarity. Keep Previous and Next in the same place on every page so they do not move under the pointer, and give every control at least a 24 by 24 pixel target. After a page change, move focus to the top of the results or the results heading so keyboard and screen reader users start reading the new page, not the old footer.
How to build one in Next.js
Put the page number in the URL. Pages become shareable, the back button works, and search engines can crawl every page:
const PAGE_SIZE = 25
export default async function Orders({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
const page = Math.max(1, Number((await searchParams).page) || 1)
const { rows, total } = await getOrders({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE })
const pageCount = Math.ceil(total / PAGE_SIZE)
return (
<>
<OrdersTable rows={rows} />
<nav aria-label="Pagination" className="flex gap-2">
{page > 1 ? <a href={"?page=" + (page - 1)}>Previous</a> : null}
<span aria-current="page">Page {page} of {pageCount}</span>
{page < pageCount ? <a href={"?page=" + (page + 1)}>Next</a> : null}
</nav>
</>
)
}
Common mistakes
- Showing every page number. Show the first, last and a window around the current page, with an ellipsis between.
- Keeping page 9 after the user narrows a filter to 3 pages of results. Reset to page 1 when filters change.
- Buttons with
onClickonly, which cannot be opened in a new tab or crawled.
MiniDev ships Pagination for general lists and TablePagination for use under a data table. Both live in the navigation category.
PaginationuiA pagination nav with Prev and Next buttons that disable at the edges and numbered page buttons that mark the current page with aria-current. Built in React.npx shadcn@latest add ui.minidev.pro/r/pagination.jsonTable PaginationuiTable pagination footer showing Page X of Y with previous and next icon buttons that disable at the first and last page. Wire it to onPageChange in React.npx shadcn@latest add ui.minidev.pro/r/table-pagination.json