Pagination
Page navigation with a configurable range and layouts.
Playground
<Pagination
currentPage={5}
showNumbers
showPreviousNext
totalPages={12}
visiblePages={5}
/>01 Layouts
layout decides what the bar is made of. Pick the one that fits
the surface. The Playground above lets you flip between them live.
layout | What it renders | When to reach for it |
|---|---|---|
default (default) | A number window with ellipses, prev / next, and ellipsis-gated first / last. | List and search-result pages. |
navigation | Previous / Next buttons only, with no page numbers. | Article or record flows where the page number does not matter. |
table | A row-count summary (e.g. "1–25 of 500") beside prev / next. | The footer of a data table. Pair it with itemsPerPage / totalItems. |
minimal | A single "Page 3 of 20" indicator, no buttons. | Tight toolbars and mobile bars. |
02 Examples
Pagination is controlled: you hold the 1-based currentPage in your own state and update it from onPageChange. The
bar reports the page the user picked and never changes it on its own.
Browsing a long list
currentPage indexes a $derived slice of the orders, so picking a page swaps the visible rows. The bar stays presentational: it reports the page and leaves the data to you.- Order #1042
- Order #1041
- Order #1040
- Order #1039
- Order #1038
- Order #1037
<script lang="ts">
import { Pagination } from '@urbicon-ui/blocks';
const perPage = 6;
let page = $state(1);
// currentPage indexes the slice; onPageChange moves it.
const visible = $derived(orders.slice((page - 1) * perPage, page * perPage));
const totalPages = Math.ceil(orders.length / perPage);
</script>
<ul>
{#each visible as order (order.id)}
<li>{order.name}</li>
{/each}
</ul>
<Pagination currentPage={page} {totalPages} visiblePages={5} onPageChange={(p) => (page = p)} />Data-table footer
layout=table swaps the number window for a row-count summary and pins prev / next to the right. totalPages still drives the buttons, while itemsPerPage and totalItems only build the summary. On the first page Previous is disabled in place.| Invoice | Customer | Amount |
|---|---|---|
| INV-1042 | Northwind Traders | $2,400.00 |
| INV-1041 | Aperture Labs | $980.00 |
| INV-1040 | Soylent Corp | $12,150.00 |
<div class="border-border-subtle w-full overflow-hidden rounded-lg border">
<table class="w-full text-left text-sm">
<thead class="text-text-secondary border-border-subtle bg-surface-quiet border-b">
<tr>
<th class="px-4 py-2 font-medium">Invoice</th>
<th class="px-4 py-2 font-medium">Customer</th>
<th class="px-4 py-2 pr-4 text-right font-medium">Amount</th>
</tr>
</thead>
<tbody class="text-text-secondary divide-border-subtle divide-y">
<tr>
<td class="px-4 py-2.5">INV-1042</td>
<td class="px-4 py-2.5">Northwind Traders</td>
<td class="px-4 py-2.5 text-right tabular-nums">$2,400.00</td>
</tr>
<tr>
<td class="px-4 py-2.5">INV-1041</td>
<td class="px-4 py-2.5">Aperture Labs</td>
<td class="px-4 py-2.5 text-right tabular-nums">$980.00</td>
</tr>
<tr>
<td class="px-4 py-2.5">INV-1040</td>
<td class="px-4 py-2.5">Soylent Corp</td>
<td class="px-4 py-2.5 text-right tabular-nums">$12,150.00</td>
</tr>
</tbody>
</table>
<div class="border-border-subtle border-t px-4 py-3">
<Pagination
currentPage={tablePage}
totalPages={48}
layout="table"
variant="ghost"
intent="neutral"
size="sm"
itemsPerPage={3}
totalItems={142}
onPageChange={(p: number) => (tablePage = p)}
/>
</div>
</div>03 Customization
Segmented bar
slotClasses gives the base slot a surface-quiet fill, a subtle border and a container radius. The buttons keep their own radius tier and behaviour.<Pagination
currentPage={customPage}
totalPages={12}
visiblePages={5}
showFirstLast={false}
slotClasses={{
base: 'border-border-subtle bg-surface-quiet w-fit rounded-lg border px-1.5 py-1'
}}
onPageChange={(p: number) => (customPage = p)}
/>This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.
04 Accessibility
Built-in ARIA
The root element is a <nav> landmark carrying
an aria-label, so assistive tech lists it as a named
navigation region. Pass your own aria-label to name
each pager when a page carries more than one. The active page button sets aria-current="page". A disabled boundary button
(Previous on the first page, Next on the last) is inert and marked aria-disabled.
Keyboard
Tab moves through the controls in DOM order. Enter / Space activates the focused one. Every enabled control (first / last, prev / next, and the numbered buttons) is reachable this way, and a disabled boundary button is skipped.
05 API Reference
37 propsProp | 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 variant classes, on the page buttons too, 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 |
06 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 | — | |
PaginationLinkVariants | type | variant | 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. |
07 Installation
Import
import { Pagination } from '@urbicon-ui/blocks';