Pagination
Page navigation with customizable range, layouts, intents, and boundary control.
Playground
<Pagination
currentPage={5}
showNumbers
showPreviousNext
totalPages={12}
visiblePages={5}
/>01 Examples
Layouts
Default — full page bar
Navigation — prev / next only
Table — info + controls
Minimal — page indicator
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">
Default — full page bar
</p>
<Pagination
currentPage={pg3}
totalPages={20}
showFirstLast
visiblePages={5}
onPageChange={(p: number) => (pg3 = p)}
/>
</div>
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">
Navigation — prev / next only
</p>
<Pagination
currentPage={pg3}
totalPages={20}
layout="navigation"
onPageChange={(p: number) => (pg3 = p)}
/>
</div>
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">
Table — info + controls
</p>
<Pagination
currentPage={pg3}
totalPages={20}
layout="table"
itemsPerPage={25}
totalItems={500}
onPageChange={(p: number) => (pg3 = p)}
/>
</div>
<div class="flex flex-col gap-2.5">
<p class="text-text-tertiary text-xs font-medium tracking-wider uppercase">
Minimal — page indicator
</p>
<Pagination currentPage={pg3} totalPages={20} layout="minimal" />
</div>Data Table Row
<div class="border-border-subtle bg-surface-elevated rounded-xl border px-5 py-3">
<Pagination
currentPage={pg6}
totalPages={42}
layout="table"
variant="ghost"
intent="neutral"
size="sm"
itemsPerPage={25}
totalItems={1042}
onPageChange={(p: number) => (pg6 = p)}
/>
</div>02 Customization
slotClasses Override
<Pagination
currentPage={pg6}
totalPages={15}
showInfo
slotClasses={{
base: 'gap-2',
info: 'text-primary font-medium',
ellipsis: 'text-danger font-bold'
}}
onPageChange={(p: number) => (pg6 = p)}
/>Pill Buttons
<Pagination
currentPage={pg6}
totalPages={10}
variant="filled"
intent="secondary"
slotClasses={{ controls: '[&>*]:rounded-full' }}
onPageChange={(p: number) => (pg6 = p)}
/>Terminal Style (unstyled)
<Pagination
unstyled
currentPage={4}
totalPages={12}
showInfo
showNumbers={false}
variant="ghost"
intent="neutral"
class="terminal-pagination flex items-center justify-between gap-4 font-mono text-sm text-emerald-300"
slotClasses={{
info: 'tabular-nums text-emerald-300',
controls: 'flex gap-2'
}}
previousLabel="← prev"
nextLabel="next →"
/>Used on more than one list, the pill or terminal styles above become one BlocksProvider preset (presets.Pagination) applied via preset — see Customization.
03 Accessibility
Built-in ARIA
The root <nav> carries role="navigation" and an aria-label. The active page button sets aria-current="page". Disabled boundary buttons expose aria-disabled so screen readers can announce their state.
Keyboard
Tab moves focus between pagination buttons in DOM order. Enter / Space activates the focused button. First/last, prev/next, and numbered buttons are all focusable in natural tab order.
Reduced Motion
Mint effects respect prefers-reduced-motion. Transitions and hover/active
feedback are reduced or removed when the user has requested less motion.
04 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
currentPage required | number | — | 1-based index of the currently active page. | |
totalPages required | number | — | Total number of pages in the data set. | |
class | string | — | Additional CSS classes merged onto the root <nav> element. | |
disabled | boolean | — | Disables all buttons and dims the component. | |
endItem | number | — | Override the calculated end item number for the info text. | |
firstIcon | Snippet | — | Custom icon rendered inside the "First" button. | |
firstLabel | string | — | Label for the "First" button. Falls back to i18n key pagination.first. | |
infoText | string | — | Override the auto-generated info text with a fully custom string. | |
intent | primarysecondarysuccesswarning +2 more | 'primary' | Semantic color applied to every pagination button. | |
itemsPerPage | number | — | Items shown per page. Used by the table layout to compute "Showing X to Y of Z". | |
lastIcon | Snippet | — | Custom icon rendered inside the "Last" button. | |
lastLabel | string | — | Label for the "Last" button. Falls back to i18n key pagination.last. | |
layout variant | defaultminimalnavigationtable | default | Controls the layout behavior and appearance of the Pagination component. Available options: default, minimal, navigation, table. | |
loading | boolean | — | Shows a loading state with reduced opacity. | |
mint | MintProp | 'none' | Micro-interaction preset forwarded to the inner buttons (page items, prev/next, first/last). | |
nextIcon | Snippet | — | Custom icon rendered inside the "Next" button. | |
nextLabel | string | — | Label for the "Next" button. Falls back to i18n key pagination.next. | |
onPageChange | (page: number) => void | — | Fires when the user selects a different page. Receives the 1-based page number. | |
pageLabel | string | — | Prefix for the info text (e.g. "Page"). Falls back to i18n key pagination.page. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Pagination: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
previousIcon | Snippet | — | Custom icon rendered inside the "Previous" button. | |
previousLabel | string | — | Label for the "Previous" button. Falls back to i18n key pagination.previous. | |
renderItem | Snippet<[PaginationItemContext]> | — | Render each numbered page button yourself. Receives a PaginationItemContext
with the page number, its active/disabled state, the forwarded style props
(size, variant, intent, tier, mint) and a select callback. Only affects the
numbered page buttons in the default layout — prev/next/first/last keep their
own icon snippets, and the ellipsis is unaffected. | |
showFirstLast | boolean | true | Show "First" / "Last" boundary buttons when the current page is far from
the edges. Deliberately redundancy-gated to the number window: the buttons
render only beside a start/end ellipsis, so without showNumbers there is
no number window, no ellipsis — and no First/Last buttons. That coupling is
intentional (a compact prev/next-only bar stays compact), not a bug.
Setting showFirstLast explicitly while showNumbers is false warns
once per instance in dev. | |
showInfo | boolean | — | Show a text summary such as "Page 3 of 10" beneath the controls. | |
showNumbers | boolean | — | Show numbered page buttons. Set to false for a compact prev/next-only bar. | |
showPreviousNext | boolean | — | Show "Previous" / "Next" navigation buttons. | |
size | smmdlg | 'md' | Button dimensions. Affects page numbers, prev/next, and first/last. | |
slotClasses | Partial<Record<PaginationSlots, string>> | — | Per-slot class overrides merged with (or replacing, when unstyled) tv styles. Slots: base, info, controls, ellipsis. | |
startItem | number | — | Override the calculated start item number for the info text. | |
tier | InteractiveTier | — | Semantic radius tier forwarded to pagination buttons. | |
totalItems | number | — | Total number of items across all pages. Used by the table layout info text. | |
unstyled | boolean | — | Strip all default tailwind-variants classes for a fully custom build. | |
variant | outlinedfilledghost | 'ghost' | Visual weight of pagination buttons. The current page carries its own
encoding regardless (a filled face on outlined, a subtle fill plus ring
on ghost), so the quiet default keeps a nine-button page window from
reading as a row of equally loud pills. | |
visiblePages | number | — | Maximum number of page buttons shown between the ellipsis indicators. | |
...HTMLAttributes<HTMLElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'class') | |
...PaginationVariants variant | VariantProps | — | Styling variants from PaginationVariants |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
PaginationPageItem | interface | helper | 0 | — | |
PaginationItemContext | interface | helper | 0 | Context handed to the renderItem snippet for a single numbered page button.
Bundles the page number, its active/disabled state, the style props forwarded
from the Pagination (so a custom item stays visually consistent), and a
select callback that changes the page (guarded against disabled / no-op /
out-of-range internally). | |
PaginationProps | interface | props | 0 | — | |
PaginationItemProps | interface | props | 0 | — | |
PaginationVariants | type | variant | 0 | — | |
PaginationSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
InteractiveTier | type | helper | 1 | Semantic radius tier for interactive surfaces (3-tier system).
- commit → r-human (CTA, identity, status declarations)
- modify → r-interactive (fields, navigation, secondary actions)
Container components (Card, Alert, Toolbar surface, …) live in a third
tier contain (r-structure) which is **not** part of this propagation
context — those surfaces are always r-structure by design and have no
tier-flip use case. | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
06 Installation
Import
import { Pagination } from '@urbicon-ui/blocks';