Build pagination with ellipsis
Build pagination for a fixed 10 pages. Printing all 10 page buttons at once doesn't scale — collapse the middle into an ellipsis instead, always keeping the first page, the last page, and the pages immediately around the current one visible.
Prev/Next move one page at a time and disable themselves at the start and end.
Examples
// page = 1, total = 10
// Rendered: [Prev] 1 2 … 10 [Next]
// Prev disabled, Next enabled.
// Click page-5
// Rendered: [Prev] 1 … 4 5 6 … 10 [Next]
// Click page-10
// Rendered: [Prev] 1 … 9 10 [Next]
// Next disabled.First + last + a 3-wide window around the current page; the gaps become ellipses.
function getPageItems(page, total) {
const items = [];
const window = new Set(
[1, total, page - 1, page, page + 1]
.filter((n) => n >= 1 && n <= total)
);
const sorted = [...window].sort((a, b) => a - b);
let prev = null;
for (const n of sorted) {
if (prev !== null && n - prev > 1) items.push({ type: "ellipsis" });
items.push({ type: "page", value: n });
prev = n;
}
return items;
}Build a sorted set of visible pages, then walk it and insert an ellipsis wherever adjacent values differ by more than 1.
What it has to do
- A `data-testid="current-page"` element shows the active page number as plain text, starting at 1.
- A `data-testid="prev"` and `data-testid="next"` button each; `prev` is `disabled` on page 1, `next` is `disabled` on page 10.
- Each visible page number is a button `data-testid="page-N"` (e.g. `page-1`, `page-10`).
- Skipped ranges render as `data-testid="ellipsis"` — with 10 total pages, not every page number can be on screen at once.
- Clicking any visible page button jumps straight to that page.
Notes
- Treat page as the single source of truth; derive the window during render. Storing "which buttons are visible" separately drifts out of sync the moment the window logic grows.
- Clamp `setPage` with `Math.max(1, Math.min(total, next))` even though the buttons are disabled — keyboard, URL params, or external callers can set out-of-range.
- Use `aria-current="page"` on the active page button so screen readers announce the current position without relying on color/weight.
- For URL-driven pagination, read `page` from the query string (`useSearchParams`) and push updates back to it — bookmarkability trumps internal state purity.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; const TOTAL_PAGES = 10; export default function App() { const [page, setPage] = useState(1); // TODO: for 10 pages this renders every button — implement windowing // instead: always show page 1 and page 10, the current page and its // immediate neighbors, and a "..." for whatever is skipped in between. const pageNumbers = Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1); return ( <div style={{ fontFamily: "sans-serif", padding: 16 }}> <p>Page: <span data-testid="current-page">{page}</span></p> <nav data-testid="pagination" style={{ display: "flex", gap: 6, flexWrap: "wrap" }}> <button data-testid="prev" disabled={page === 1} onClick={() => setPage((p) => Math.max(1, p - 1))} > Prev </button> {pageNumbers.map((n) => ( <button key={n} data-testid={`page-${n}`} aria-current={n === page ? "page" : undefined} onClick={() => setPage(n)} style={{ fontWeight: n === page ? "bold" : "normal" }} > {n} </button> ))} <button data-testid="next" disabled={page === TOTAL_PAGES} onClick={() => setPage((p) => Math.min(TOTAL_PAGES, p + 1))} > Next </button> </nav> </div> ); }
- 1.A `data-testid="current-page"` element shows the active page number as plain text, starting at 1.
- 2.A `data-testid="prev"` and `data-testid="next"` button each; `prev` is `disabled` on page 1, `next` is `disabled` on page 10.
- 3.Each visible page number is a button `data-testid="page-N"` (e.g. `page-1`, `page-10`).
- 4.Skipped ranges render as `data-testid="ellipsis"` — with 10 total pages, not every page number can be on screen at once.
- 5.Clicking any visible page button jumps straight to that page.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
Discussion
Sign in to postNo comments yet. Be the first to say something.