Pagination Component
Machine-coding brief for pagination — page math, truncated ranges, controlled page, a11y navigation, and edge cases.
- machine-coding
- interview
- react
- a11y
Problem statement
Build a pagination control: given total items and page size (or total pages), render page links with prev/next and ellipsis truncation. Interviewers score off-by-one math, controlled state, and accessibility of the nav landmark.
Requirements
Must have
- Controlled
page(1-based) +onChange totalPagesor compute fromtotalItems+pageSize- Prev / Next disabled at ends
- Numbered page buttons
- Truncation with ellipsis for large page counts (window around current)
Should have
- First / last controls
aria-current="page"on active- Compact mobile variant (only prev/next + status)
Nice to have
- Page size selector
- Jump-to-page input
- URL sync
Planning (5 minutes out loud)
- 1-based page in the API (human-friendly)
- Pure function
getPageItems(current, total, siblingCount) - MVP — prev/next + all pages if total ≤ 7; then ellipsis algorithm
- Empty totalPages edge case
- Don’t fetch data inside — pagination is presentational
Architecture
Pagination
├── PrevButton
├── PageButton[] / Ellipsis
└── NextButton
API
type PaginationProps = {
page: number; // 1-based
totalPages: number;
onChange: (page: number) => void;
siblingCount?: number; // default 1
disabled?: boolean;
};
Implementation sketch
Range builder
type Item = number | "ellipsis";
export function getPaginationItems(
page: number,
totalPages: number,
siblingCount = 1
): Item[] {
if (totalPages <= 1) return totalPages === 1 ? [1] : [];
const totalNumbers = siblingCount * 2 + 5; // first, last, current, 2 ellipsis
if (totalPages <= totalNumbers) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const left = Math.max(page - siblingCount, 1);
const right = Math.min(page + siblingCount, totalPages);
const showLeftEllipsis = left > 2;
const showRightEllipsis = right < totalPages - 1;
const items: Item[] = [1];
if (showLeftEllipsis) items.push("ellipsis");
else {
for (let i = 2; i < left; i++) items.push(i);
}
for (let i = left; i <= right; i++) {
if (i !== 1 && i !== totalPages) items.push(i);
}
if (showRightEllipsis) items.push("ellipsis");
else {
for (let i = right + 1; i < totalPages; i++) items.push(i);
}
if (totalPages > 1) items.push(totalPages);
// Dedupe / fix edge overlaps for small windows near ends
return uniquePreserve(items);
}
function uniquePreserve(items: Item[]): Item[] {
const out: Item[] = [];
for (const it of items) {
if (it === "ellipsis") {
if (out[out.length - 1] !== "ellipsis") out.push(it);
continue;
}
if (!out.includes(it)) out.push(it);
}
return out;
}
Simpler interview variant: always show 1 … current-1 current current+1 … last with clamps — easier to get right under time pressure.
Component
function Pagination({
page,
totalPages,
onChange,
siblingCount = 1,
disabled,
}: PaginationProps) {
if (totalPages <= 0) return null;
const items = getPaginationItems(page, totalPages, siblingCount);
return (
<nav aria-label="Pagination">
<button
type="button"
aria-label="Previous page"
disabled={disabled || page <= 1}
onClick={() => onChange(page - 1)}
>
Prev
</button>
<ul>
{items.map((item, i) =>
item === "ellipsis" ? (
<li key={`e-${i}`} aria-hidden>
…
</li>
) : (
<li key={item}>
<button
type="button"
aria-label={`Page ${item}`}
aria-current={item === page ? "page" : undefined}
disabled={disabled}
onClick={() => onChange(item)}
>
{item}
</button>
</li>
)
)}
</ul>
<button
type="button"
aria-label="Next page"
disabled={disabled || page >= totalPages}
onClick={() => onChange(page + 1)}
>
Next
</button>
</nav>
);
}
Deriving totalPages
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
When totalItems === 0, product choice: hide pagination or show page 1 empty state.
Accessibility essentials
- Wrap in
<nav aria-label="Pagination"> aria-current="page"on current- Buttons disabled at ends (not just visually grey)
- Ellipsis not focusable
- Announce page change in the results region (parent’s job):
Showing 21–40 of 390
Performance notes
- Pure function; memo if parent re-renders often
- No virtualization concerns
Footguns
- 0-based vs 1-based mismatch with API
- Duplicate page numbers near edges in ellipsis math
- totalPages 0 division issues
- Stale page when filters shrink totalPages — parent should clamp
- Using links without href strategy when SEO needs real URLs — prefer
<a href="?page=">for MPA
Interview out-loud answer
Pagination is controlled page state with a pure range builder that inserts ellipses. Prev/next disable at ends; current page gets aria-current. Data fetching stays in the parent with cursor or offset APIs. I’d clamp page when totalPages shrinks after filtering and keep the math unit-tested with table cases.
Related on this site
- Sortable Data Table
- Product Filters Sidebar
- Search Results Page Design
- Machine Coding Interview Framework