Pagination
Bounded page controls move through a collection while keeping its current page explicit.
On this page
- Engineering note 1
Illustrative report · documentation example
- Engineering note 2
Illustrative report · documentation example
- Engineering note 3
Illustrative report · documentation example
Showing report page 1 of 12.
Code
import { Pagination, type PaginationLabels } from "@bosunkit/components/pagination";
import { Switch } from "@bosunkit/components/switch";
import { useId, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A local report collection changes with the page. Compact mode retains previous, next and a readable summary. All entries are illustrative.";
const labels: PaginationLabels = {
previous: "Previous report page",
next: "Next report page",
first: "First report page",
last: "Last report page",
page: (page: number): string => `Report page ${String(page)}`,
summary: (page: number, total: number): string => `Page ${String(page)} of ${String(total)}`,
};
export default function Showcase(): ReactElement {
const [page, setPage]: [number, Dispatch<SetStateAction<number>>] = useState(1);
const [compact, setCompact]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
const results: string = useId();
return (
<div style={{ display: "grid", gap: "var(--space-lg)", width: "100%" }}>
<label style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
<Switch checked={compact} onCheckedChange={setCompact} />
Compact pagination
</label>
<ul id={results} style={{ display: "grid", gap: "var(--space-md)", margin: 0, padding: 0, listStyle: "none" }}>
{[0, 1, 2].map((offset: number): ReactElement => (
<li key={offset} style={{ padding: "var(--space-md)", borderBottom: "1px solid var(--border)" }}>
<strong>Engineering note {String((page - 1) * 3 + offset + 1)}</strong>
<p style={{ margin: 0, color: "var(--text-dim)" }}>Illustrative report · documentation example</p>
</li>
))}
</ul>
<p role="status">Showing report page {String(page)} of 12.</p>
<Pagination
label="Report collection pages"
pageCount={12}
value={page}
onValueChange={setPage}
labels={labels}
controls={results}
compact={compact}
/>
</div>
);
}
Pagination controls a collection with a known number of pages. It renders numbered pages, noninteractive gaps, previous and next controls, and optional first and last shortcuts. Loading the results belongs to the caller.
Examples
Boundaries, loading and empty collections
The first page disables Previous; the final page disables Next. Zero pages exposes the caller's empty summary and disables every direction. A one-page collection has one current-page control and unavailable directions.
Empty collection
Single page
Code
import { Pagination, type PaginationLabels } from "@bosunkit/components/pagination";
import { Switch } from "@bosunkit/components/switch";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"First/last shortcuts, disabled loading controls, an empty collection and a one-page collection. Empty is distinct from a loading total that is not known.";
const labels: PaginationLabels = {
previous: "Previous page",
next: "Next page",
first: "First page",
last: "Last page",
page: (page: number): string => `Page ${String(page)}`,
summary: (page: number, total: number): string =>
total === 0 ? "No pages" : `Page ${String(page)} of ${String(total)}`,
};
export default function States(): ReactElement {
const [disabled, setDisabled]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
return (
<div style={{ display: "grid", gap: "var(--space-xl)" }}>
<label style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
<Switch checked={disabled} onCheckedChange={setDisabled} />
Disable controls while loading
</label>
<Pagination
label="Large collection"
pageCount={48}
defaultValue={24}
labels={labels}
showFirstLast
disabled={disabled}
/>
<div>
<p>Empty collection</p>
<Pagination label="Empty collection" pageCount={0} labels={labels} />
</div>
<div>
<p>Single page</p>
<Pagination label="Single-page collection" pageCount={1} labels={labels} />
</div>
</div>
);
}
Real links and browser history
The address selects collection page 1 of 5.
Code
import { Pagination, type PaginationLabels } from "@bosunkit/components/pagination";
import { useId, useSyncExternalStore, type ReactElement } from "react";
export const description: string =
"These are real links. Change page, then use browser Back and Forward: the address fragment owns this example's current page.";
const labels: PaginationLabels = {
previous: "Previous linked page",
next: "Next linked page",
first: "First linked page",
last: "Last linked page",
page: (page: number): string => `Linked page ${String(page)}`,
summary: (page: number, total: number): string => `Page ${String(page)} of ${String(total)}`,
};
function subscribe(change: () => void): () => void {
window.addEventListener("hashchange", change);
return (): void => {
window.removeEventListener("hashchange", change);
};
}
function snapshot(): number {
const match: RegExpExecArray | null = /^#collection-page=([1-5])$/.exec(window.location.hash);
return match === null ? 1 : Number(match[1]);
}
function serverSnapshot(): number {
return 1;
}
export default function Links(): ReactElement {
const page: number = useSyncExternalStore(subscribe, snapshot, serverSnapshot);
const results: string = useId();
return (
<div style={{ display: "grid", gap: "var(--space-lg)", width: "100%" }}>
<p id={results} role="status">
The address selects collection page {String(page)} of 5.
</p>
<Pagination
label="Linked collection pages"
pageCount={5}
value={page}
labels={labels}
controls={results}
getPageHref={(value: number): string => `#collection-page=${String(value)}`}
/>
</div>
);
}
Local state or route state
Pass value and onValueChange when the application owns the page. Omit value to start from defaultValue and
keep local state. Pages are one-based. The displayed current page is bounded to the available range; this does not
write corrected state back to the caller or issue a request during rendering.
For URL pagination, supply getPageHref. Each available destination becomes a real link through the provider's
adapter. The callback runs for an ordinary primary click; modified clicks retain browser behavior and do not
change local selection. The router owns location, browser Back/Forward and fetching. Read page from that URL.
<Pagination
label={t('reports.pages')}
value={pageFromUrl}
pageCount={pageCount}
labels={translatedPaginationLabels}
getPageHref={(page) => makeReportUrl({ page, filter, sort })}
controls="report-results"
/>Button mode is appropriate for a local collection or a controlled view with no separate page URL. Neither mode moves focus into results automatically. Keep focus on the activated control during loading, then announce a short result summary. Move focus only when the application intentionally treats the change as a new page.
Density and responsive behavior
The default window includes one nearby page on each side. siblingCount accepts zero through two; the rendered
window stays bounded even for very large totals. Gaps are text, not hidden navigation actions.
compact shows previous, summary and next, retaining first/last shortcuts if requested. A bk-screen container
at the phone breakpoint selects this form automatically. Controls wrap when no screen container is supplied.
The summary formatter must explain both current and total, including the empty case.
Data contract
Use a finite, safe integer pageCount. Zero and invalid counts render the empty state. An unknown total is a
different state: show loading feedback or caller-owned previous/next controls until the service provides a total.
Do not present an unknown total as zero results.
When filters, sort order or page size change, update the page and query together. Cancel superseded requests,
retain the chosen page until the response is accepted, and use disabled for a pending operation when further
requests are unavailable. Keep errors near the results and provide retry without silently changing pages.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | Name of this navigation landmark. |
| pageCountrequired | number | – | Total known pages. Zero or an invalid count produces the empty, disabled state. |
| value | number | undefined | – | Current one-based page; omit for uncontrolled use. Out-of-range values are bounded for display only. |
| defaultValue | number | undefined | 1 | Initial page when uncontrolled; one by default. |
| onValueChange | ((page: number) => void) | undefined | – | Requests a page; the caller owns loading, URL state and result focus. |
| labelsrequired | PaginationLabels | – | Translated control names and summary formatters. |
| formatPage | ((page: number) => string) | undefined | String | Optional display formatter for page numerals; defaults to ungrouped integer digits. |
| getPageHref | ((page: number) => string) | undefined | – | Real URLs for route pagination; modified clicks retain normal browser behavior. |
| controls | string | undefined | – | Id of the results region controlled by these controls. |
| disabled | boolean | undefined | false | Disables navigation, for example while the caller loads a requested page. |
| compact | boolean | undefined | false | Show only previous, the page summary and next; narrow screen containers also use this form. |
| showFirstLast | boolean | undefined | false | Include first and last controls; false by default. |
| siblingCount | number | undefined | 1 | Nearby pages on either side of the current page, bounded to zero through two; one by default. |
| className | string | undefined | – | Additional root classes. |
| testId | string | undefined | – | Root test id. |
| ref | Ref<HTMLElement> | undefined | – | The navigation landmark. |
Engineering reports · Page 3 of 12
Next page opens a different collection.
Related
- Navigation: collection paging alongside other navigation.
- Data table: tabular results and sorting.
- Entity list: records with identity and actions.
- Search input: query changes and result feedback.
Usage reference: Atlassian pagination.