Pagination
Page controls for a paged set of results — deliberately presentational, plus one helper for the range arithmetic that is easy to get subtly wrong.
Usage#
import {
getPaginationRange,
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "neelam-ui";
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious disabled={page === 1} onClick={previous} />
</PaginationItem>
{getPaginationRange({ currentPage: page, totalPages }).map((item, index) => (
<PaginationItem key={item === "ellipsis" ? `gap-${index}` : item}>
{item === "ellipsis" ? (
<PaginationEllipsis />
) : (
<PaginationLink isActive={item === page} onClick={() => setPage(item)}>
{item}
</PaginationLink>
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext disabled={page === totalPages} onClick={next} />
</PaginationItem>
</PaginationContent>
</Pagination>Presentational by design#
There is no internal "current page" state and no context threading it around.
In a real app the current page already lives in a URL or query state the caller
owns, so inventing a second copy here would only create something to keep in
sync. PaginationLink's isActive and getPaginationRange are how that
external state becomes markup.
The same reasoning applies to PaginationPrevious and PaginationNext: pass
disabled yourself, because the component has no page count to derive it from.
getPaginationRange#
The one piece worth providing. It computes which page numbers to show and where runs of skipped pages collapse:
getPaginationRange({ currentPage: 6, totalPages: 20 });
// [1, "ellipsis", 5, 6, 7, "ellipsis", 20]siblingCount (default 1) controls how many numbers sit on each side of the
current page. Getting the boundaries right near the start and end — so the range
does not flicker into "1 2 3 … 3" and similar off-by-ones — is common enough to
get wrong that it is worth not re-deriving in every app.
Buttons, not links#
PaginationLink renders a native <button>, despite the name. An <a> with no
href is not keyboard-focusable, and whether changing page should actually
navigate — versus updating local or query state without a URL change — varies by
app. So this does not assume a URL the way
BreadcrumbLink reasonably can.
If your pagination does navigate
Render your router's link inside PaginationLink, or swap the element for an
<a href> of your own and keep the same classes. Real URLs per page are
better for sharing and for the back button when the option is open to you.
Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Moves to the next control. Every page button is its own tab stop. |
| EnterSpace | Activates the focused control — native button behaviour. |
There is no arrow-key roving here. These are ordinary buttons in a nav landmark, not a composite widget, so each is independently reachable.
Accessibility#
- The wrapper is
<nav aria-label="pagination">, so it is reachable by landmark. LikeBreadcrumb's, this label can be hardcoded because the meaning never varies. - The current page carries
aria-current="page", driven byisActive. PaginationEllipsisis decorative and hidden from assistive tech — the gap is a rendering detail, not information.- Disabled previous/next controls use the native
disabledattribute, so they are skipped byTabrather than being focusable dead ends.
Announce the page change
Moving to a new page usually replaces content elsewhere, which is silent for a screen reader user. Move focus to the results heading after a page change, or announce it in a live region — the pagination controls alone cannot do this for you.
API reference#
getPaginationRange#
| Prop | Type | Default |
|---|---|---|
currentPagerequiredThe current page, 1-indexed. | number | — |
totalPagesrequired | number | — |
siblingCountHow many page numbers to show on each side of the current page. Defaults to `1`. | number | — |
Returns Array<number | "ellipsis">.
PaginationLink#
| Prop | Type | Default |
|---|---|---|
isActiveMarks this as the current page: sets `aria-current="page"` and applies the active look. | boolean | — |
Everything else#
Pagination (<nav>), PaginationContent (<ul>), PaginationItem (<li>),
PaginationPrevious and PaginationNext (<button>), and PaginationEllipsis
add no props of their own — every native attribute passes through.