Import
import { TablePagination } from "@timelycare/helix-ui"
Props
interface TablePaginationProps {
page: number // 1-based
pageSize: number
total: number
onPageChange: (page: number) => void
hasPrev?: boolean // override the derived guard
hasNext?: boolean // override the derived guard
label?: (range: { start: number; end: number; total: number }) => React.ReactNode
previousLabel?: React.ReactNode // default "Previous"
nextLabel?: React.ReactNode // default "Next"
className?: string
}
Usage
<Table>{/* ... */}</Table>
<TablePagination
page={page}
pageSize={10}
total={total}
onPageChange={setPage}
/>
Renders nothing when total is 0, and hides the buttons when everything fits on one page, so it is safe to render unconditionally under a table.
Selection count instead of a range
<TablePagination
page={page}
pageSize={10}
total={total}
onPageChange={setPage}
label={() => `${selected.length} of ${total} row(s) selected.`}
/>
Inside a card
The default spacing is py-4, matching the footer documented on the DataTable page. When the footer sits inside a container that owns its own padding, override it:
<TablePagination className="px-6 pt-6" … />
Structure
| Element | Tailwind |
|---|---|
| Root | flex items-center justify-between py-4 |
| Status line | text-muted-foreground text-sm + role="status" aria-live="polite" aria-atomic="true" |
| Button group | space-x-2 |
| Buttons | Button variant="outline" size="sm" |
Why a component rather than a copy-paste footer
The DataTable page documents this footer as markup you assemble. Shipping it removes two recurring problems:
- The range arithmetic is easy to get subtly wrong. The first index is off by one if you forget the
+ 1, and the last page needs clamping against the total. - The hand-rolled version is silent. Paging changes the status text, but nothing announces it. Here the status line is a polite, atomic live region, so the whole new range is read out rather than nothing at all.
Choosing a Pagination Style
| Situation | Use |
|---|---|
| Previous/Next with a row range | TablePagination |
| Numbered page links, first/last jumps | Pagination |
| Load more / infinite scroll | A single Button; these are not pages |
Accessibility
- The status line is
role="status"witharia-live="polite"andaria-atomic="true", so a page change announces the complete range instead of the digits that changed. - Buttons are disabled rather than hidden at the ends of the range, so their position does not shift under the pointer or in the tab order.
- Focus stays on the pressed button across a page change, so paging repeatedly by keyboard does not require re-finding the control.
- Pass
previousLabel,nextLabelandlabelalready translated; the library does no i18n.
Gotchas
| Problem | Solution |
|---|---|
| Range starts at -9 | page is 1-based; a 0-based index was passed |
| Buttons never enable | Cursor-based API with an unknown total; drive them with hasPrev / hasNext |
| Range announced twice | The status line is already a live region; remove the outer one |
| Footer padding fights the card | Override with className; the default py-4 assumes the footer owns its spacing |
| No rows-per-page selector | Not included; add one beside the component if a product needs it |
Related
- Related Components: Pagination (numbered links), Table, DataTable (where this footer is documented)
- Patterns: Table Behavior