Browse docs
Browse docs
A controlled pagination control. The consumer owns page and
pageSize; the component emits onPageChange / onPageSizeChange
and recomputes the visible page range from totalCount.
import { Pagination } from '@dashforge/tw';
import { useState } from 'react';
function MyList() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
return (
<Pagination
page={page}
pageSize={pageSize}
totalCount={1437}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
);
}The default variant renders the full kit: summary text, page-number buttons, first / prev / next / last navigators, page-size selector, and direct jump input.
import { useState } from 'react';
import { Pagination } from '@dashforge/tw';
function Paginated() {
const [page, setPage] = useState(3);
const [pageSize, setPageSize] = useState(20);
return (
<Pagination
page={page}
pageSize={pageSize}
totalCount={247}
onPageChange={setPage}
onPageSizeChange={setPageSize}
pageSizeOptions={[10, 20, 50, 100]}
/>
);
}variant | Renders |
|---|---|
default (default) | Summary · page numbers · first/prev/next/last · page-size selector · jump input |
compact | Page numbers · first/prev/next/last only — no summary, no selector, no jump |
minimal | "Page X of Y" text · prev/next only — for narrow surfaces (mobile, dense toolbars) |
import { useState } from 'react';
import { Pagination, Stack } from '@dashforge/tw';
function Variants() {
const [pageA, setPageA] = useState(5);
const [pageB, setPageB] = useState(5);
return (
<Stack gap={3}>
<Pagination page={pageA} pageSize={20} totalCount={247}
onPageChange={setPageA} variant="compact" />
<Pagination page={pageB} pageSize={20} totalCount={247}
onPageChange={setPageB} variant="minimal" />
</Stack>
);
}Three steps (sm / md / lg) drive padding + text size on every interactive element. Pick the size that visually balances the table or list it's attached to.
import { useState } from 'react';
import { Pagination, Stack } from '@dashforge/tw';
function SizesShowcase() {
const [pageA, setPageA] = useState(3);
const [pageB, setPageB] = useState(3);
const [pageC, setPageC] = useState(3);
return (
<Stack gap={3}>
<Pagination page={pageA} pageSize={20} totalCount={247}
onPageChange={setPageA} size="sm" variant="compact" />
<Pagination page={pageB} pageSize={20} totalCount={247}
onPageChange={setPageB} size="md" variant="compact" />
<Pagination page={pageC} pageSize={20} totalCount={247}
onPageChange={setPageC} size="lg" variant="compact" />
</Stack>
);
}| Prop | Type | Notes |
|---|---|---|
page | number | 1-indexed. The component clamps out-of-range values internally (e.g. page=99 on a 5-page set renders page 5). |
pageSize | number | Items per page. |
totalCount | number | Total items in the underlying data set. NOT total pages — we compute that from totalCount / pageSize. |
onPageChange | (page: number) => void | Fires on every page interaction (button click, jump input commit). |
<Pagination
page={page}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={setPage}
onPageSizeChange={setPageSize} // omit to hide the selector
pageSizeOptions={[10, 20, 50, 100]} // default
siblingCount={1} // pages shown each side of current
boundaryCount={1} // pages always shown at start/end
showFirstLast // default true
showJumpInput // default true (default variant only)
disabled={false} // disables all interactive bits
/>If the total set is small enough to fit within
2 * boundaryCount + 2 * siblingCount + 3 pages, no ellipsis is
shown — every page button renders explicitly. So with defaults
(siblingCount=1, boundaryCount=1), totals of ≤ 7 pages render
all numbers directly; 8+ pages get the ellipsis layout.
All visible strings are configurable via labels. Pass the subset
you need; the rest fall back to English defaults.
<Pagination
page={page}
pageSize={pageSize}
totalCount={250}
onPageChange={setPage}
labels={{
first: 'Inizio',
prev: 'Indietro',
next: 'Avanti',
last: 'Fine',
page: 'Pagina',
of: 'di',
showing: 'Mostrando',
perPage: 'per pagina',
goToPage: 'Vai a pagina',
}}
/>Default English labels:
| Key | Default | Used in |
|---|---|---|
first | 'First' | aria-label of « button |
prev | 'Previous' | aria-label of ‹ button |
next | 'Next' | aria-label of › button |
last | 'Last' | aria-label of » button |
page | 'Page' | "Page N" aria-label + minimal-mode summary |
of | 'of' | "Showing X-Y of Z" + minimal "Page X of Y" |
showing | 'Showing' | Default-variant summary prefix |
perPage | 'per page' | Page-size selector label |
goToPage | 'Go to page' | Jump input placeholder + label |
<nav aria-label="Pagination"> — screen
reader announces it as a pagination navigation region.aria-current="page" so screen
readers announce the current position.aria-label
(page number, "Previous", "Next", "First", "Last", "Go to page").<select> element — native a11y
semantics (arrow keys, Enter, screen reader announces options).<input type="number"> with min / max
matching the page range. Commits on Enter or blur.In variant="default" the rightmost input lets the user jump to
any page directly:
<Pagination
page={page}
pageSize={pageSize}
totalCount={1437}
onPageChange={setPage}
showJumpInput // default; pass `false` to hide
/>Behavior:
[1, totalPages]<Pagination> is fully controlled by design — the same page
pageSize state can drive both a <Pagination> UI and the
underlying list / table. Sprint 4.1 will ship a Table + DataGrid
that explicitly compose with Pagination via the same controlled
state.const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const paginatedRows = useMemo(
() => allRows.slice((page - 1) * pageSize, page * pageSize),
[allRows, page, pageSize],
);
return (
<>
{/* Sprint 4.1 — Table coming */}
<YourList rows={paginatedRows} />
<Pagination
page={page}
pageSize={pageSize}
totalCount={allRows.length}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</>
);Pagination follows the same sx + slotProps dual-override
pattern as every other @dashforge/tw component. See the
Customization guide for the
full decision tree.
sx — outer wrapper<Pagination sx="justify-end gap-6" {...props} />slotPropsAvailable slots: root, summary, list, pageButton,
activeButton, navButton, ellipsis, pageSizeSelector,
jumpInput.
<Pagination
page={page}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={setPage}
slotProps={{
activeButton: { className: 'bg-emerald-600 border-emerald-600' },
summary: { className: 'font-mono uppercase tracking-wide' },
}}
/>Configure <Pagination> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Pagination: {
defaults: { variant: 'default', size: 'md' },
},
},
});Configurable axes (PaginationVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'default' | 'compact' | 'minimal' | UI mode — default = full toolbar, compact = numbers + nav, minimal = "Page X of Y" + prev/next. |
size | 'sm' | 'md' | 'lg' | Button + font-size density. |
Non-visual axes (page, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, siblingCount, boundaryCount, showFirstLast, showJumpInput, disabled, labels) are per-instance — data / behavior / i18n — not theme-configurable.
Precedence chain (lowest → highest):
defaultVariants from the internal paginationVariants recipe (variant: 'default', size: 'md').theme.components.Pagination.defaults (application-wide).<Pagination variant="compact" size="sm" />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.Pagination.defaults autocompletes to the two axes above.
Reactivity: useComponentDefaults('Pagination') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
onPageChange | (page: number) => void | — | Fired when the user picks a different page. |
page | number | — | Current page, 1-indexed. |
pageSize | number | — | Items per page. |
totalCount | number | — | Total items in the underlying data set (NOT total pages — we compute that). |
boundaryCount | number | 1 | How many page buttons to always show at the start/end of the sequence (acts as "boundary"). |
disabled | boolean | — | Disable all interactive elements (kept rendered but inert). |
labels | PaginationLabels | — | I18n strings. Defaults are English; pass the subset you need to translate. |
onPageSizeChange | (pageSize: number) => void | — | Fired when the user picks a different page size. Omit (or pass undefined) to hide the page-size selector. |
pageSizeOptions | number[] | [10, 20, 50, 100] | Options shown in the page-size selector. |
showFirstLast | boolean | true | Show first/last buttons (« / »). |
showJumpInput | boolean | true | Show the direct page-jump input (variant=default only). |
siblingCount | number | 1 | How many page buttons to show on each side of the current page. |
size | 'md' | 'sm' | 'lg' | 'md' | Visual density tier for buttons and font-size. |
slotProps | PaginationSlotProps | — | Per-slot overrides. |
sx | string | — | Root-level Tailwind override. |
variant | 'compact' | 'default' | 'minimal' | 'default' | UI mode — default shows the full toolbar, compact drops the summary + selector + jump input, minimal renders "Page X of Y" with prev/next only. |
<Pagination> is a compound component with named slots for each toolbar region. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer <nav aria-label="Pagination"> wrapper. |
summary | The "Showing X–Y of Z" text block (default variant only). |
pageSizeSelector | The <select> for changing page size. |
pageList | The <ul> wrapping page-number buttons + ellipses. |
pageButton | Each individual page-number button. |
activeButton | The currently-active page button. |
navButton | Prev / Next / First / Last navigation buttons. |
jumpInput | The direct page-jump <input> (default variant only). |
ellipsis | The … non-interactive gap marker. |
Use sx for a root-level class override; use slotProps when you need to reach a specific region (e.g. active button color, summary font style).
sx vs slotProps decision tree