Dev 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 · Dev view
Dev

Code anatomy

Slot Code slot Semantic
nav-root nav-root nav-with-aria-label
items-list items-list ul
previous-button previous-button a-or-button
next-button next-button a-or-button
page-button page-button a-or-button
current-marker current-marker a-with-aria-current
ellipsis ellipsis span-aria-hidden
first-button first-button a-or-button
last-button last-button a-or-button
page-context-text page-context-text span-or-aria-live
load-more-button load-more-button button
page-size-selector page-size-selector select
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)
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-pagination>` host with `variant`, `size`, `current-page`, `total-pages`, `sibling-count`, `boundary-count`, `show-first-last`, `disabled` attributes; renders the navigation landmark plus the items-list with computed page-buttons + ellipses based on the truncation algorithm. The wire-name `ui-pagination` becomes `md-pagination` / `mat-paginator` / `pf-pagination` per design system; the canonical wire-name stays `pagination`. Slotted `previous-label`, `next-label`, `page-label-template` for i18n customisation. Attributes drive variant + properties (`variant="numbered"`, `size="md"`, `show-first-last`, `disabled`); `current-page` and `total-pages` reflect to attributes for declarative state. CSS uses logical properties so RTL flips chevron direction. Page-change emits `page-change` CustomEvent.
React Compound component (Material UI Pagination, Atlassian Pagination, Carbon Pagination, Polaris Pagination as third-party precedents). React Aria ships no Pagination primitive — consumers compose `<a>` / `<button>` per page using the underlying `useFocusRing` and `usePress` hooks. Material UI exposes `count`, `page`, `siblingCount`, `boundaryCount`, `showFirstButton`, `showLastButton`, `onChange(event, page)` as the canonical API surface. The compound shape (`<Pagination.Previous>`, `<Pagination.Item>`) exists in HeroUI / Mantine. Props with class-variance-authority for variant + size classes; `siblingCount` + `boundaryCount` drive the truncation algorithm. `getItemAriaLabel` callback for i18n of "Go to page N" labels. Server-side rendering pairs with framework `<Link>` (Next.js, React Router) so the rendered DOM is `<a href>` not `<button>`.
Angular (signals) Angular component with `input<PaginationVariant>('variant')`, `input<number>('currentPage')`, `input<number>('totalPages')`, `input<number>('siblingCount')`, `input<number>('boundaryCount')`, `input<boolean>('showFirstLast')`, `input<boolean>('disabled')`. Angular Material ships `MatPaginator` with the same surface plus `pageSize` / `pageSizeOptions` for the page-size- selector slot. Page-change via `output<{ pageIndex: number, pageSize: number }>('pageChange')` plus optional two-way `model<number>('currentPage')`. Host-binding `[attr.role]="'navigation'"` plus `[attr.aria-label]`; signal-driven reactive truncation — `computed()` derives the visible page-numbers from `currentPage()`, `totalPages()`, `siblingCount()`, `boundaryCount()`. CSS variables drive size scaling so consumers override per-instance.
Vue Single-file component with default + named slots for previous-label / next-label / item-template / ellipsis customisation. PrimeVue Paginator, Element Plus Pagination, Naive UI Pagination as third-party precedents. `defineProps` exposes `currentPage` (v-model), `totalPages`, `siblingCount`, `boundaryCount`, `showFirstLast`, `disabled`. Page-change via `@update:current-page` (v-model:currentPage). `defineProps` with literal-union types (`variant: 'numbered' | 'prev-next-only' | 'load-more'`); v-model:currentPage and v-model:pageSize for two-way state binding. Slot-based item-template lets consumers override per-page-button rendering without prop-drilling.
Both

Events

  1. pageChange
    Payload
    `{ page: number, originalEvent: MouseEvent | KeyboardEvent }`. Fires when the user activates a page-button, previous, next, first, or last button. The `page` value is the 1-based page index the user navigated to. Consumer updates the data model and re-renders; the pagination reflects the new `aria-current="page"` on the activated page-button.
    Web Components
    `pageChange` CustomEvent on the host with `event.detail = { page, originalEvent }`.
    React
    `onChange(event, page)` callback (Material UI idiom) or `onPageChange(page)` (Polaris).
    Angular Signals
    `output<number>('pageChange')` plus two-way `model<number>('currentPage')`.
    Vue
    `@update:current-page` (v-model:currentPage) with payload `number`.
  2. pageSizeChangeoptional
    Payload
    `{ pageSize: number, page: number }`. Fires when the user changes the page-size selector. Canonical convention: reset `page` to 1 on size change so the user does not land on an empty page (page 5 at size 50 may not exist at size 100).
    Web Components
    `pageSizeChange` CustomEvent with `event.detail = { pageSize, page }`.
    React
    `onPageSizeChange(size)` callback or `onChange({ pageSize })` (Material UI's TablePagination idiom).
    Angular Signals
    `output<{ pageSize: number; page: number }>('pageSizeChange')` on the MatPaginator (Angular Material).
    Vue
    `@update:page-size` (v-model:pageSize) plus `@update:current-page` reset to 1.
Dev

Form integration

name attribute
Pagination is a navigation component, not a form control. The `<nav>` element has no `name` attribute and contributes nothing to FormData. Page state lives in the URL query (server-rendered canonical) or in component state (SPA fallback) — not in the form payload.
Both

Accessibility

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.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus moves through the pagination items in DOM order — first-button (if present), previous- button, page-buttons (with current-marker included as a tab-stop unless rendered as non-interactive span), next-button, last-button (if present), page-size-selector (if present). Disabled buttons retain their tab-stop position; focus lands on them and SR announces "dimmed".
Shift+TabReverse focus through the same items. Standard tab-order — no pagination-specific behaviour.
Enter / Space (focus on page-button / previous / next)Activates the button. For `<a href>` page- buttons, Enter navigates (Space does nothing — anchors do not respond to Space by spec). For `<button>`, both Enter and Space activate. pageChange fires.
Enter / Space (focus on disabled button)No-op. The element is in DOM with `aria-disabled="true"` so AT announces "unavailable"; activation is suppressed via `event.preventDefault` in the handler. Tab-order remains stable.
Enter / Space (focus on load-more-button, load-more variant)Activates the load-more action. `aria-busy="true"` while fetching; on resolve, focus moves to the first newly-appended row OR an `aria-live` region announces the delta. Without one of these signals, keyboard users are stranded.

Screen-reader announcements

TriggerExpected
SR enters the pagination landmarkSR announces "navigation, Pagination" (the landmark plus its label). For the numbered variant, the announcement continues with "list, N items" (the items-list) and the user can navigate from there.
SR encounters the current-markerSR announces "current page, Page 3" — the `aria-current="page"` semantic plus the accessible name. Distinct from page-buttons which announce "Page 4, link".
SR encounters a disabled previous / nextSR announces "Previous, dimmed" or "unavailable". The element retains its position in the announcement stream so users learn the pattern stays put.
User activates a page-button (server-paginated)Server fetch begins. `aria-busy="true"` on the nav-root or on the data surface; SR may announce "loading" via the live-region. On resolve, the new page renders and `aria-current="page"` moves to the activated button. Focus stays on the activated button so users can continue navigation without re-finding their place.
User activates load-more-button (load-more variant)SR announces "loading" (via aria-busy or live- region). On resolve, an aria-live region announces the row-count delta ("20 more results loaded, 60 total"). Focus moves to the first newly-appended row OR stays on the load-more button (per the implementation's chosen pattern; both are canonical, document the choice).

axe-core rules to assert

  • aria-allowed-attr
  • aria-required-attr
  • aria-valid-attr-value
  • landmark-unique
  • link-name
  • button-name
  • color-contrast

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/pagination/a11y-fixture.json

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.
Dev

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.

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.