Skip to main content

Pagination

Allows users to navigate through large data sets split across multiple pages. Use when a list or table contains more items than can be shown in a single view. Don't use for continuous scroll interfaces.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?Notes
Previous button Required Navigates to the preceding page. Disabled on the first page. Contains a left-arrow icon and a visible label for accessibility.
Next button Required Navigates to the following page. Disabled on the last page. Contains a right-arrow icon and a visible label for accessibility.
Page number buttons Required Clickable page numbers. The current page is visually distinguished (variant="neutral" appearance="accent" — solid fill, aria-current='page'). Inactive pages use appearance="outlined" so they still read as pressable. Never use appearance="filled" for either — its fill has no border and fails contrast (WCAG 1.4.11) against every surface in this theme.
Ellipsis indicator Recommended A non-interactive '...' element shown when the page range is truncated. Must not be keyboard-focusable. Appears between the first/last page and the windowed range.
First page shortcut Optional Always-visible link to page 1. Recommended when the total page count exceeds 10.
Last page shortcut Optional Always-visible link to the final page. Recommended when the total page count exceeds 10.
Page count summary Recommended Plain text showing context such as 'Page 3 of 47' or 'Showing 41–60 of 930 results'. Assists screen reader users and sighted users scanning quickly.
Items-per-page selector Optional A Select component letting the user choose how many rows appear per page (e.g. 20, 50, 100). Position above or below the pagination bar, aligned to the opposite side.

Variants

States

State Behaviour
Default All applicable page buttons and Previous/Next buttons are interactive. Current page button shows filled --op-color-interactive-default background with white label.
Previous disabled Previous button has pointer-events: none, opacity reduced to --op-color-text-disabled, and aria-disabled='true'. Occurs on page 1.
Next disabled Next button has pointer-events: none, opacity reduced to --op-color-text-disabled, and aria-disabled='true'. Occurs on the last page.
Page button hover Background transitions to --op-color-interactive-hover at 150ms ease. Cursor: pointer.
Page button focus 3px solid focus ring using --op-color-interactive-focus, 2px offset. Visible on keyboard navigation.
Current page Filled background --op-color-interactive-default, text --op-color-text-on-interactive, aria-current='page'.
Loading All buttons disabled and aria-busy='true' on the nav element while a new page of results is fetching. A wa-spinner can appear adjacent to the summary text.

Usage guidelines

Do / Don't

Do

Show a page count summary ('Page 3 of 47') so users always know where they are.

Don't

Show page numbers alone without any context about the total number of pages.

Do

Preserve the current page in the URL query string (e.g. ?page=3) so users can bookmark or share a specific page and the browser back button works correctly.

Don't

Manage page state in JavaScript memory only, which breaks the browser back button and makes links unshareable.

Do

Disable (not hide) the Previous button on page 1 and the Next button on the last page.

Don't

Hide Previous/Next when they are unavailable — this causes layout shift and confuses users about the control's purpose.

Do

Keep the items-per-page selector close to the pagination bar and label it clearly (e.g. 'Rows per page').

Don't

Place items-per-page in a settings panel separate from the table — users won't find it.

Do

Announce page changes to screen readers by moving focus to the top of the updated content region or using an aria-live region.

Don't

Let the page update silently — screen reader users will not know the content has changed.

Layout & Spacing

The pagination bar is a flex row, align-items: center, gap: --op-space-4 (4px) between page buttons. Outer padding: --op-space-12 (12px) top and bottom, flush with the table edge horizontally. Page number buttons: min-width 36px, height 36px, padding: 0 --op-space-8 (8px). Touch targets expanded to 44x44px via padding or pseudo-element. Previous / Next buttons: height 36px, padding: 0 --op-space-12 (12px), gap --op-space-4 between icon and label. Page count summary: margin-left: auto (right-aligned) or rendered in a separate flex row above the bar. Items-per-page selector: float opposite side to page numbers, or placed above the table right-aligned.

Tokens

PartTokenValue
Current page button background --op-color-interactive-default green-400; use for the active page indicator
Current page button text --op-color-text-on-interactive white; must meet 4.5:1 against --op-color-interactive-default
Inactive page button text --op-color-text-primary Default text colour for non-current page numbers
Inactive page button background transparent No background on rest state; avoid adding a border to inactive page buttons
Page button hover background --op-color-interactive-hover Light green tint; applied on :hover and :focus-visible for inactive buttons
Disabled button text --op-color-text-disabled Applied to Previous on page 1 and Next on last page
Focus ring --op-color-interactive-focus 3px solid, 2px offset, on all focusable buttons
Page count summary text --op-color-text-secondary Slightly subdued; reinforces it is metadata not a control
Ellipsis text --op-color-text-secondary Non-interactive; do not apply hover or focus styles
Container border (optional separator from table) --op-color-border-default 1px top border if pagination bar sits below a borderless table

Engineering notes

  • Wrap the entire control in a <nav> element with aria-label='Pagination'. This creates a landmark for screen reader navigation.
  • Render page number buttons as <a> tags with an href pointing to the page URL (e.g. ?page=4) — not <button> elements — so middle-click, Ctrl+click, and browser history work correctly. Fall back to buttons only for SPA client-side routing where URLs cannot change.
  • Set aria-current='page' on the active page link. Do not rely on visual styling alone.
  • Set aria-disabled='true' and tabindex='-1' on the Previous/Next buttons when at the boundary. Do not use the HTML disabled attribute on <a> tags.
  • Use aria-label on icon-only Previous/Next buttons (e.g. aria-label='Previous page'). If buttons include visible text, the icon is aria-hidden='true'.
  • When the page changes via JavaScript (SPA), move focus to the heading or first interactive element of the new content, or update an aria-live='polite' region with the new page summary.
  • Persist page state in the URL (?page=N) and read it on mount so that page refresh, back/forward navigation, and shared links all restore the correct page.
  • The items-per-page selector should reset the page to 1 when the value changes to avoid showing an out-of-range page.
  • For the windowed page range: always show pages 1 and N, show a window of currentPage ± 2, insert ellipsis where there is a gap larger than 1.

Keyboard interaction

KeyAction
Tab Moves focus through Previous button, page number links, ellipsis (skipped — not focusable), Next button, and items-per-page selector in DOM order.
Enter Activates the focused page link or button, navigating to that page.
Space Activates a focused button (equivalent to Enter). No effect on links.
Shift + Tab Moves focus in reverse through the pagination controls.

Why it matters

Government users include people using screen readers, keyboard-only navigation, or voice control software. Pagination controls that lack proper semantics or focus management leave these users unable to access data beyond the first page — a critical failure in applications like stakeholder registers or public consultation portals.

Focus

On initial render, focus is not moved to the pagination bar — it belongs to the page content above. When a page change is triggered by keyboard:

  • In MPA / full-page navigation: the browser naturally moves focus to the top of the new document. Ensure the page title or a skip link is the first focusable element.
  • In SPA client-side navigation: after the content updates, programmatically move focus to the heading of the updated results region (e.g. the table caption or a visually hidden h2 reading 'Page 3 results'). Do not leave focus on the pagination bar after navigation. The focus ring (3px solid --op-color-interactive-focus, 2px offset) must be visible on all interactive elements including the current page button if it remains focusable.

ARIA

Role or attributeWhen to useExample
nav[aria-label] Wraps the entire pagination component to create a navigation landmark. <nav aria-label="Pagination">
aria-current Set to 'page' on the active page link. <a href="?page=3" aria-current="page">3</a>
aria-disabled Set to 'true' on Previous/Next anchors when at the boundary. Combine with tabindex='-1'. <a aria-disabled="true" tabindex="-1" aria-label="Previous page">
aria-label (button) Provide an accessible name for icon-only or ambiguous buttons. <button aria-label="Next page"><wa-icon name="chevron-right" aria-hidden="true"></wa-icon></button>
aria-hidden Applied to decorative icons inside labelled buttons and to ellipsis characters. <span aria-hidden="true">…</span>
aria-live Optional polite live region to announce page changes in SPA contexts without moving focus. <div aria-live="polite" aria-atomic="true" class="sr-only">Page 3 of 47</div>
aria-busy Set to 'true' on the nav element while a new page of results is loading. <nav aria-label="Pagination" aria-busy="true">

Contrast

Current page button: --op-color-text-on-interactive (white) on --op-color-interactive-default (green-400). Must meet 4.5:1 for normal text. Verify the specific green-400 value in tokens before shipping. Inactive page numbers: --op-color-text-primary on --op-color-bg-primary. Must meet 4.5:1. Disabled Previous/Next: --op-color-text-disabled on --op-color-bg-primary. Disabled controls are exempt from contrast requirements under WCAG 2.1 but should still be distinguishable from interactive controls. Page count summary: --op-color-text-secondary on --op-color-bg-primary. Must meet 4.5:1 — secondary text is not exempt. Focus ring: --op-color-interactive-focus against adjacent background. Must meet 3:1 (WCAG 2.1 AA non-text contrast).

Touch targets

All interactive elements (Previous, page numbers, Next, items-per-page) must have a minimum 44x44px touch target. Where the visual button is smaller (e.g. 36px), extend the tap area using padding or a ::after pseudo-element without affecting layout. On mobile breakpoints, switch to the Simple variant to reduce the number of small targets in a row.

Things to avoid

  • Relying solely on colour to indicate the current page — always pair colour with a visible border, bold weight, or other non-colour indicator.
  • Auto-advancing pages without user interaction (e.g. a timed carousel-style paginator) — this violates WCAG 2.2 success criterion 2.2.2.
  • Using <span> or <div> elements as interactive page buttons without role='button' and keyboard event handlers.
  • Placing the pagination nav inside the table element or its caption — it is a sibling of the table, not part of it.
  • Omitting the aria-label on the <nav> element when there are multiple nav landmarks on the page — without a label, screen readers cannot distinguish 'Pagination' from 'Main navigation'.

Was this page helpful?

Updated 5 October 2026