Command Palette

Search for a command to run...

Pagination

A list footer that summarizes the visible range and steps between pages.

For tables, reach for DataTable.Pagination — the ready-made instance of this pattern. Compose it by hand, as shown below, when you're paginating something that isn't a table (for example, a cursor-based list).

When to use

Use pagination as the footer of a paged list or table: it tells people where they are in the full set and lets them move through it. Pair it with a list that loads one page at a time — not with a short, fully-loaded list where everything already fits on screen.

Anatomy

The pattern is a single row with two halves:

  • Range summary (leading) — Showing 1–20 of 1,240 <noun>. The visible range and the total, so the count is legible at a glance.
  • Page controls (trailing) — a Rows per page Select and three icon Buttons: first page (|<), previous (<), and next (>).

This hand-composed instance has no skip-to-last button, because it targets forward-only cursor lists where an arbitrary last page can't be addressed reliably (see Guidelines). Instances that do know their page count — like the DataTable.Pagination table instance — can offer one.

Usage

import { Button } from "@smartacteam/ambient-web/button";
import { IconArrowWallLeft } from "@smartacteam/ambient-web/icons/icon-arrow-wall-left";
import { IconChevronLeft } from "@smartacteam/ambient-web/icons/icon-chevron-left";
import { IconChevronRight } from "@smartacteam/ambient-web/icons/icon-chevron-right";
import { Select } from "@smartacteam/ambient-web/select";
import { TabularNum } from "@smartacteam/ambient-web/tabular-num";
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
  <p className="text-sm text-text-secondary">
    Showing <TabularNum>{rangeStart.toLocaleString()}</TabularNum>
    <TabularNum>{rangeEnd.toLocaleString()}</TabularNum> of{" "}
    <TabularNum>{total.toLocaleString()}</TabularNum> customers
  </p>
  <div className="flex items-center gap-0.5">
    <div className="flex items-center gap-2">
      <p className="text-sm text-text-secondary">Rows per page</p>
      <Select.Root value={`${pageSize}`} onValueChange={onPageSizeChange}>
        <Select.Trigger size="sm">
          <Select.Value placeholder={pageSize} />
        </Select.Trigger>
        <Select.Content side="top">
          {[10, 20, 30, 40, 50].map((size) => (
            <Select.Item key={size} value={`${size}`}>
              {size}
            </Select.Item>
          ))}
        </Select.Content>
      </Select.Root>
    </div>
    <div className="flex items-center gap-0.5">
      <Button
        variant="ghost"
        size="icon-sm"
        onClick={onFirstPage}
        disabled={!hasPreviousPage}
      >
        <span className="sr-only">Go to first page</span>
        <IconArrowWallLeft />
      </Button>
      <Button
        variant="ghost"
        size="icon-sm"
        onClick={onPreviousPage}
        disabled={!hasPreviousPage}
      >
        <span className="sr-only">Go to previous page</span>
        <IconChevronLeft />
      </Button>
      <Button
        variant="ghost"
        size="icon-sm"
        onClick={onNextPage}
        disabled={!hasNextPage}
      >
        <span className="sr-only">Go to next page</span>
        <IconChevronRight />
      </Button>
    </div>
  </div>
</div>

Guidelines

  • Wrap the numbers in TabularNum. The range and total change as people page through; tabular figures keep the digits from shifting width as they update.
  • Localize the numbers. Use toLocaleString() (or your i18n number formatter) so large totals read as 1,240, not 1240.
  • Source the values from the list, not the UI. The total feeds “of Y” from the query's totalCount; hasPreviousPage / hasNextPage come from the cursor PageInfo — derive the disabled states from those, never from a guess at the page count.
  • Skip-to-last only when the last page is addressable. Forward-only cursors have no cursor for an arbitrary last page, so this instance omits the control — it would be a dead end or a lie. Instances backed by a known page count (a client-side table) can include it. The first page is always reachable (the null cursor), so |< always stays.
  • Every icon button needs a label. The arrows are icon-only, so each carries an sr-only span (“Go to first / previous / next page”) for screen readers.
  • Hide it when it can't do anything. If the whole list fits on one page, render nothing rather than a row of disabled controls.