Skip to main content
Urbicon UI

Pagination

Page navigation with a configurable range and layouts.

Playground

Layout Style variant
Variant
Size
Mint
<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.

layoutWhat it rendersWhen to reach for it
default (default)A number window with ellipses, prev / next, and ellipsis-gated first / last.List and search-result pages.
navigationPrevious / Next buttons only, with no page numbers.Article or record flows where the page number does not matter.
tableA row-count summary (e.g. "1–25 of 500") beside prev / next.The footer of a data table. Pair it with itemsPerPage / totalItems.
minimalA 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

Slice your data by the current page. Here 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.
InvoiceCustomerAmount
INV-1042Northwind Traders$2,400.00
INV-1041Aperture Labs$980.00
INV-1040Soylent 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

Group the whole pager into one tinted segment: 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 props
37 props 2 required
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

12 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Pagination } from '@urbicon-ui/blocks';