Designer view

Pagination

A navigation component for moving between discrete pages of a larger dataset — search results, table rows, document indices. Wrapped in a `<nav>` landmark with a specific `aria-label` ("Pagination"); current page marked with `aria-current="page"`; disabled previous / next controls retain DOM presence with `aria-disabled="true"` so the tab-order stays stable as users page through. Three structural variants — numbered (the page list), prev-next-only (single previous + next plus a "Page X of N" context), and load-more (single button that appends the next page below the current data).

Also called Pager Page navigation

When to use

Use

For datasets larger than a single viewport where users move between bounded pages — search results, table rows, document indices, blog archives. Use the numbered variant when the total page count is bounded and known (typically 1–50 pages) so users see absolute position. Use prev-next-only when the total is large or unknown (server logs, infinite-feeling feeds with checkpoints). Use load-more when the workflow favours append-style discovery (image galleries, social feeds) where users build up a longer continuous list rather than navigating discrete pages.

Avoid

For switching between sibling sections of one page — that is `Tabs`. For sequential workflows where step-N depends on step-N-1 — that is `Stepper`. For traversing site hierarchy — that is `Breadcrumbs`. For purely presentational page-of-N labels without navigation — render static text, not pagination. For client-side pagination over already-loaded data smaller than a screen — render the full list and use grouping or filtering instead.

Versus related

  • table

    `Pagination` partners with `Table` to chunk large datasets — pagination is the navigation control that sits adjacent to the table; the table is the data surface. They appear together but are distinct components: pagination handles "which page" while the table handles "how the data renders". Use both together for paginated tables; use Table alone when all rows fit (or when virtualization replaces pagination as the size-handling strategy).

  • tabs

    `Tabs` switches between sibling sections of the same logical page (overview / activity / files for one project); `Pagination` traverses pages of one logical list (results 1-20 / 21-40 / 41-60). Tabs surface different content under the same heading; pagination moves the same content's window across the dataset. The decision test: do the surfaces present different kinds of content (Tabs) or different windows on the same content (Pagination)?

  • stepper

    `Stepper` represents a linear sequence with state carried between steps (cart → address → payment); `Pagination` lets users move freely between pages of a list without sequence semantics. Stepper has progress meaning (steps before are completed, steps after are pending); pagination has only location meaning (page 3 of 12, no progress). Never use pagination for a flow.

  • breadcrumbs

    `Breadcrumbs` orients users within a site hierarchy (Home → Settings → Account); `Pagination` orients users within a single list's page-window. Breadcrumbs cross site contexts; pagination stays in one context. They often appear together — breadcrumbs at the top of the page, pagination below the data.

Pagination decomposes a large list into bounded pages so users scan in chunks rather than scrolling indefinitely. The reference documents the nav-landmark contract, the `aria-current="page"` rule on the current-page item (rendered as static or as a non- navigating link, never as a self-link to the current URL), the ellipsis-as-decorative discipline, and the disabled-not-removed contract that keeps tab-order stable across page transitions. Truncation uses Material's siblingCount + boundaryCount API as the canonical configuration. Distinct from Tabs (in-page section switching) and Stepper (linear progress) — pagination is navigation across page boundaries within one logical content surface.

Highlight
Fig 1.1 · Pagination · Designer view
Designer

Figma anatomy

Slot Figma type Hint
nav-root frame Auto-layout horizontal frame; navigation landmark variant property
items-list frame Horizontal auto-layout list; gap small; reset list-marker
previous-button instance Button instance with leading chevron-left icon plus optional "Previous" label
next-button instance Button instance with trailing chevron-right icon plus optional "Next" label
page-button instance Page-number button instance with default / hover / focus-visible / current variants
current-marker instance Current-page button variant; non-link styling; aria-current state
ellipsis text Decorative ellipsis ("…") character; not a button instance
first-button instance Button instance with double-chevron-left icon
last-button instance Button instance with double-chevron-right icon
page-context-text text Inline text "Page X of N" or "Showing N-M of T"; static text style
load-more-button instance Button instance with optional "Loading..." state and progress indicator
page-size-selector instance Select instance with page-size options
Designer

Token usage per slot

current-marker
typography
  • weightweight.semibold
ellipsis
color
  • foregroundcolor.text.muted
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps numbered / prev-next-only / load-more.
SizeEnumsizesm / md.
Current PageNumbercurrentPage1-based current page index. Drives `aria-current="page"` placement and atFirst / atLast state.
Total PagesNumbertotalPages1-based total page count. Drives the visible page-button range and atLast state.
Sibling CountNumbersiblingCountNumber of page-buttons shown adjacent to the current page (default 1; Material UI convention).
Boundary CountNumberboundaryCountNumber of page-buttons shown at the start / end of the range before ellipses appear (default 1; Material UI convention).
Show First LastBooleanshowFirstLastWhen true, first / last boundary-jump buttons render at the extremes of the items-list.
DisabledBooleandisabledWhole pagination is non-interactive; all buttons render with `aria-disabled="true"`.
Page SizeNumberpageSizeItems-per-page when the page-size-selector slot is authored. Pairs with pageSizeChange event.
Get Item Aria LabelTextgetItemAriaLabeli18n callback that returns the accessible name for each item ("Go to page N", "Previous page", etc.).
Designer

Motion

TransitionDuration token
pageTransitionmotion.duration.fast
loadMoreAppendmotion.duration.base
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smBelow this width, the canonical pattern collapses the numbered variant to prev-next-only — page-buttons hide, only previous + page-context- text + next remain. Polaris Pagination's mobile mode is the precedent. The variant change is progressive enhancement rather than authored breakpoint-state — the same `<nav>` renders differently per viewport via CSS or via per- breakpoint variant choice.
breakpoint.mdAt and above this width, the numbered variant renders with the full visible page-button range per `siblingCount + boundaryCount`. Boundary- jump buttons (first / last) appear when `showFirstLast` is true and total page-count exceeds the visible range.
Both

Internationalisation

RTL · mirroring

Chevron icons (chevron-left for previous, chevron-right for next, double-chevron for first / last) flip direction under RTL — visual previous becomes right-pointing in Arabic / Hebrew. Implementation via CSS logical properties (`inset-inline-start`) or via icon components that mirror via the `direction` cascade. Page-numbers do not flip (digits read left-to-right even in RTL contexts via bidi). Items-list flow direction follows document direction so the leading item (typically previous-button) sits at the inline-start. Nav-landmark `aria-label` translates per locale.

Text expansion

Page-button labels expand under translation only when text labels are authored ("Previous" → "Précédent" → "السابق") — icon-only buttons are stable. "Page X of N" expands 30-50% ("Seite 3 von 12", "Page 3 sur 12"); reserve flexible space in the page-context-text slot. Number formatting via `Intl.NumberFormat` handles locale-specific thousand-separators (1,000 → 1.000 in German). For very long total-page-counts (10,000+ pages), the prev-next-only variant avoids visual crowding regardless of language.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

numbered prev-next-only load-more

Properties

The same component, parameterised.

PropertyType
size sm | md
showFirstLast boolean
disabled boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
idleloadingatFirstatLastsinglePage
Both

State transitions

FromToTrigger
idleloadingUser activates a page-button / previous / next / load-more on a server-paginated dataset
loadingidleAsync fetch resolves with new page data
idleatFirstUser navigates to page 1 (previous-button + first-button become aria-disabled)
idleatLastUser navigates to the last page (next-button + last-button become aria-disabled)
Both

Figma↔Code mismatches

  1. 01
    Figma

    Disabled previous / next drawn as removed or hidden from layout

    Code

    `aria-disabled="true"` plus visually-disabled treatment, element stays in DOM

    Consequence

    Designers compose pagination by hiding the previous button on page 1 and the next button on the last page; developers keep the buttons in the DOM with visually-disabled styling. The two surfaces shape tab-order differently — designer's hide shifts the tab-order between pages (focus jumps land in different places), developer's keep preserves a stable tab-order. Without explicit anatomy, the disabled-not-removed contract is invisible in the design file and implementations may ship the wrong pattern.

    Correct

    Document the disabled state as a structural slot property in the anatomy. Figma carries an "atFirst" and "atLast" state of the previous / next buttons with visually-disabled styling. Code ships `aria-disabled="true"` plus the disabled visual treatment without removing the element. Tab-order stays stable across page transitions.

  2. 02
    Figma

    Page-numbers drawn as styled text without explicit link affordance

    Code

    `<a href="?page=N">` (server-render canonical) or `<button>` (SPA)

    Consequence

    Designers author page-numbers as inline text styled to look clickable; developers wrap them as `<a>` (server-render) or `<button>` (SPA). The two surfaces diverge on URL semantics — anchor preserves right-click + open-in-new-tab + share-link, button breaks all three. Without explicit anatomy, the design file does not communicate the URL contract and implementations may ship buttons-everywhere by default.

    Correct

    Document the page-button as `<a href>` for server-paginated tables (the canonical default) and `<button>` only for SPA-only contexts where URL change is undesirable. Figma carries the link affordance in the page-button instance — hover- underline, accent color, visible focus ring on focus-visible. Code ships `<a href="?page=N">` whenever the page can be deep-linked.

  3. 03
    Figma

    Ellipsis drawn as decorative dots

    Code

    `<span aria-hidden="true">…</span>`, not a tab stop

    Consequence

    Designers draw "…" as static text between page boundaries; developers ship it as either a non- interactive span (canonical) or as a clickable jump-button (Material UI's non-canonical extension). The two implementations diverge on keyboard behaviour — span has no tab stop, button does. Without canonical anatomy, the design surface cannot disambiguate and implementations may ship the ellipsis-as-button pattern that confuses keyboard users.

    Correct

    Document the ellipsis as a decorative slot with `aria-hidden="true"` and no tab stop. Figma carries the ellipsis as a static text element, not a button instance. Code ships `<span aria-hidden="true">` or a CSS pseudo-element. Implementations that want jump-to-page behaviour should add a separate visible "Jump to page" input, not overload the ellipsis.

  4. 04
    Figma

    Current-page styled identically to other page-numbers

    Code

    `<a aria-current="page">` with distinct visual treatment

    Consequence

    Designers draw the current page with the same hover- style as other page-numbers; developers add `aria-current="page"` and a distinct visual treatment (filled background, bolder weight). The two surfaces visually under-communicate the current state — sighted users may not see "I am here" if the design file does not carry an explicit current variant.

    Correct

    Document the current-marker as a distinct slot with explicit visual treatment (filled background, bolder weight, accent color) AND `aria-current="page"` semantic. Figma carries a "current" variant of the page-button instance with non-link styling and the current-state visual treatment. Code wires `aria-current="page"` and removes the href so the element is not a self-link.

  5. 05
    Figma

    Load-more variant drawn identically to numbered pagination

    Code

    Distinct anatomy — single button replaces items-list, previous, next entirely

    Consequence

    Designers compose load-more pagination as a number-list with a "Load more" button at the end; developers ship it as a single button with no number-list. The two surfaces diverge structurally — load-more's data model is append (rows accumulate), numbered's is replace (rows swap per page). Without explicit variant anatomy, the design surface cannot communicate which model the implementation should use.

    Correct

    Document load-more as a distinct variant with load-more-button as the canonical interactive affordance and result-summary / page-context-text replacing the items-list. Figma carries a "Load more" variant of the pagination component with its own composition. Code ships `<button>Load more</button>` plus an `aria-live` region announcing the row-count delta on append.

Both

Contracts

Non-negotiable contracts

  1. WCAGWCAG 1.3.1 Info and Relationships

    Pagination is wrapped in `<nav>` with a specific `aria-label` ("Pagination" or the locale- translated equivalent). The label distinguishes the pagination from other navigation landmarks on the page.

    Without the landmark, SR users cannot jump to the pagination via landmark navigation — the affordance becomes invisible to AT until users encounter it by accident in linear reading order. WCAG 1.3.1 violation.

  2. WCAGWCAG 4.1.2 Name, Role, Value

    The current page carries `aria-current="page"` and is rendered as static text or as a non- navigating anchor — never as `<a href>` to the current URL. Implementations that ship a self- link create a useless tab stop and the page-reload-on-click is hostile.

    Without `aria-current="page"`, SR users hear "Page 3, link" on every page-button without any indication of which page they are currently viewing. Self-link to current page wastes user effort and breaks the navigation model.

  3. Canon

    Disabled previous / next / first / last buttons retain DOM presence with `aria-disabled="true"` — never removed via `display: none` or omitted from rendering on boundary pages.

    Without the rule, the pagination's tab-order shifts between pages — users learn one keyboard pattern then have it broken on transitions. AT users hear different element-counts in the landmark across page boundaries. The disabled-not-removed contract is canonical.

  4. Canon

    Ellipses are decorative — `aria-hidden="true"` and not in the tab-order. Implementations that ship ellipsis-as-button (Material UI's non-canonical extension) confuse keyboard users with extra tab stops and unclear behaviour.

    Without the rule, keyboard users encounter intermediate tab stops between boundary pages and sibling pages with no clear semantic. AT users hear "ellipsis, button" with no affordance contract. The decorative-ellipsis discipline is canonical.

  5. HTML specHTML standard — anchor element semantics for navigation

    Page-numbers are `<a href>` for server-paginated or URL-deep-linkable contexts; `<button>` only for SPA-only contexts where URL change is undesirable. SPA framework `<Link>` components (Next Link, React Router Link) render as anchors and satisfy the rule.

    Without the rule, right-click → "Open in new tab" does nothing; hover shows no URL preview; share-link copies the wrong URL. The affordance functions for keyboard / click but loses every URL-based interaction users expect from page navigation.

Vocabulary drift

WAI-ARIA
aria-current="page" + nav-landmark
WAI-ARIA does not ship a formal Pagination APG pattern — pagination composes from the nav landmark (`role="navigation"`) plus `aria-current="page"` on the current page. The canonical anatomy follows this composition rather than inventing pagination-specific ARIA roles.
Material 3
Pagination (Material UI extension)
Material 3 spec does not include Pagination as a first-class component — it ships as a Material UI extension. The canonical siblingCount + boundaryCount truncation API derives from Material UI's `Pagination` component (`count` / `siblingCount` / `boundaryCount` / `showFirstButton` / `showLastButton`). Mobile-first Material guidance favours infinite-scroll over pagination for content feeds.
Carbon
Pagination
Carbon ships Pagination with `page` / `pageSize` / `pageSizes` (array of page-size options) / `totalItems` API. Pairs natively with Carbon DataTable. Adds explicit page-size-selector + result-summary as first-class anatomy (canonical optional slots).
Atlassian
Pagination
Atlassian ships Pagination with `pages` (array of pages) + `selectedIndex` + `onChange(event, newSelectedPage)` API. Naming-only divergence from Material's page-as-number convention.
Polaris
Pagination
Polaris ships Pagination with `hasPrevious` / `hasNext` / `onPrevious` / `onNext` API — prev-next-only by design, not numbered. The canonical prev-next-only variant captures this Polaris-style as a first-class axis.
GitHub Primer
Pagination
Primer ships Pagination with `pageCount` / `currentPage` / `onPageChange` API plus `marginPageCount` (Primer's name for boundaryCount) and `surroundingPageCount` (Primer's name for siblingCount). Naming- only divergence; surfaces map to canonical boundaryCount + siblingCount.
GOV.UK
Pagination
GOV.UK ships Pagination with the numbered variant plus explicit "Previous page" / "Next page" labels (text not icon-only). Block-level pattern with previous + next as separate large blocks; canonical nav-root + items-list + previous / next composition matches.
React Aria
— (no Pagination primitive)
React Aria ships no Pagination component. Consumers compose pagination from `<a>` / `<button>` plus `useFocusRing` + `usePress` hooks. The explicit absence is itself canonical — pagination is a composition over standard navigation elements, not a new ARIA pattern.
Designer

Common mistakes

Blocker

#pagination-no-nav-landmark

Pagination is not wrapped in `<nav>` with `aria-label`

Problem

The pagination renders as a bare `<ul>` or a sequence of inline buttons without an enclosing landmark. SR users navigating by landmark cannot locate the pagination; the affordance is invisible to AT until users read every page-button by accident.

Fix

Wrap the pagination in `<nav aria-label="Pagination">` (or the locale-translated equivalent). The label must be specific — generic "Navigation" collides with the primary nav landmark on the page. AT users jump to "navigation, Pagination" via landmark shortcuts.

Blocker

#pagination-current-no-aria-current

Current page is not marked with `aria-current="page"`

Problem

The current page is visually distinguished (filled background, bolder weight) but the programmatic state is missing — no `aria-current="page"` attribute. SR users hear "Page 3, button" on every page-button without any indication of which page they are currently viewing. WCAG 4.1.2 Name-Role-Value violation.

Fix

Set `aria-current="page"` on the current page- button. The element should be a non-navigating anchor (`<a aria-current="page">` without href) or a static span — never `<a href="?page=3">` when already on page 3. SR announces "current page, Page 3"; sighted users see the current visual treatment.

Major

#pagination-ellipsis-focusable

Ellipsis is a tab stop or interactive element

Problem

The ellipsis between boundary pages is rendered as a button or as a focusable element with `tabindex` — Tab lands on it, SR announces it as "ellipsis, button". The behaviour is unclear (does activating jump to a specific page? open a menu?) and inconsistent across implementations.

Fix

Render the ellipsis as a non-interactive decorative element — `<span aria-hidden="true">…</span>` or a CSS pseudo-element. Remove any `tabindex`, `role="button"`, or click handler. Keyboard users navigate from boundary pages to sibling pages directly without an intermediate stop.

Major

#pagination-disabled-removed-from-tab-order

Disabled previous / next is removed from the DOM or hidden

Problem

On page 1 the previous-button is removed from the DOM (or styled `display: none`); on the last page the next-button is removed. The pagination's tab- order shifts between pages — focus lands on the first page-number on page 1 but on the previous- button on page 2. Users learn one keyboard pattern then have it broken on page transitions.

Fix

Keep disabled previous / next buttons in the DOM with `aria-disabled="true"` plus visually-disabled styling. The element retains its tab-stop position and AT announces "previous, dimmed" / "unavailable". Tab-order stays stable across page transitions. Same rule applies to optional first / last boundary-jump buttons.

Major

#pagination-no-page-context

Pagination provides no SR context for current position

Problem

The pagination renders only icon-based previous / next buttons (no text labels, no aria-label, no "Page X of N" text). SR users hear only "previous" / "next" with no indication of where they are or where they are going. Common in icon-only minimalist designs that prioritize visual cleanliness over information density.

Fix

Add `aria-label` to icon-only buttons ("Go to previous page", "Go to next page") and provide page-context-text either visibly ("Page 3 of 12") or via sr-only span. For prev-next-only variant, the page-context-text is required, not optional. The text wraps in `<span aria-live="polite">` so page changes announce on transition without stealing focus.

Accessibility hints
Slot Accessibility hint
nav-root `<nav aria-label="Pagination">` is canonical. Do not omit the label — a page may carry multiple nav landmarks (primary nav, breadcrumbs, in-page TOC, pagination). Translate the label per locale ("Paginierung" for German, "Pagination" for French) so SR announces it naturally.
items-list `<ul>` reset to no markers and no indent. Each previous / page / ellipsis / next item is wrapped in `<li>`. Some implementations omit the list and render flat anchors — this works visually but loses the "list of N items" announcement that helps SR users orient.
previous-button Accessible name: "Previous" or "Go to previous page" (icon-only buttons require `aria-label` because the chevron alone is not a name). When disabled, set `aria-disabled="true"` AND keep the element in the DOM with `tabindex="0"` (or default for `<button>`) so the tab-order is stable. Disabled controls announce "Previous, dimmed" / "unavailable"; users learn the pattern stays put across page transitions.
next-button Mirrors previous-button. Accessible name "Next" or "Go to next page". When on the last page, `aria-disabled="true"` plus visually-disabled treatment; element stays in the DOM. Some libraries add `aria-hidden` on disabled state — never do this because the user loses the announcement that there is no next page.
page-button Accessible name "Page N" (or "Go to page N") not just "N" — SR users hear isolated numbers without context otherwise. The current page is rendered via the current-marker slot (separate, with `aria-current="page"`); page-buttons are non-current and never carry `aria-current`. Visible focus ring per WCAG 2.4.7.
current-marker `aria-current="page"` is canonical. The element SHOULD NOT be `<a href="?page=3">` when already on page 3 — `aria-current` plus a self-link creates a useless tab stop and the page-reload-on-click is hostile. Render as `<a aria-current="page">` without `href`, or as `<span aria-current="page">`. SR announces "current page, Page 3".
ellipsis `<span aria-hidden="true">…</span>` or render via CSS pseudo-element. Never make ellipsis interactive in canonical pagination — Material UI's "ellipsis-becomes-button-on-click-to-jump" is a non-canonical extension that confuses keyboard users (extra tab stops with unclear behaviour). SR users hear the boundary pages and current page; ellipsis is visual scaffolding only.
first-button Accessible name "Go to first page" — the double- chevron icon needs an explicit `aria-label`. Same disabled-not-removed contract as previous-button; AT users hear "first page, dimmed" on page 1.
last-button Accessible name "Go to last page". Mirrors first- button rules. Total page-count is required input for this affordance — do not author last-button when total is unknown (open-ended feeds).
page-context-text For prev-next-only variant, wrap in `<span aria-live="polite">` so SR announces page changes without forcing focus. Number formatting follows locale (`Intl.NumberFormat`). The text serves as an `aria-labelledby` target for prev / next buttons in some implementations (e.g., next button labelled "Next page, currently on page 3 of 12").
load-more-button `<button>` with accessible name "Load more" or "Show more results". On activation, loading state sets `aria-busy="true"` until fetch resolves; on resolve, focus moves to the first newly-appended row OR an `aria-live` region announces the row count delta ("20 more results loaded, 60 total"). Without one of these signals, SR users are stranded — they activate the button but cannot find the new content.
page-size-selector Accessible name "Rows per page" via `<label>` association. Native `<select>` carries the right keyboard model and AT semantics without ARIA. Activating fires the pageSizeChange event; consumer typically resets to page 1 on change to avoid showing an empty page.