Description
Divides a long list or table into discrete pages and provides controls for moving between them, showing the user their current position within the full data set.
Below tables and lists in Open Point — stakeholder registers, consultation submission lists, activity logs, and search results. In Social Point, below community comment feeds and submission indexes on public-facing project pages.
Loading hundreds of rows at once degrades performance and overwhelms users. Pagination keeps page weight low and gives users a clear sense of progress through a data set, which is important in government workflows where lists can run to thousands of stakeholder records or submissions.
Anatomy
| Part | Required? | 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
Full (with first/last shortcuts and ellipsis)
Shows page 1, an ellipsis, a window of pages around the current page, another ellipsis, and the last page.
Data sets with more than 10 pages — common in stakeholder registers or large submission lists.
Compact (window only)
Shows only the windowed page range without first/last shortcuts. Suitable when total page count is low.
Tables with 2–10 pages where shortcuts add no value.
Simple (previous / next only)
No numbered page buttons; just Previous and Next with a page count summary.
Mobile breakpoints, or when exact page jumping is not required (e.g. chronological activity feeds).
Mini
Reduced-height version using icon-only Previous/Next buttons and a minimal 'X of Y' label. No page number buttons.
Tight layouts such as embedded card-level pagination or drawer content.
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
When to use
- A table or list contains more rows than the chosen page size (typically 20–50 rows for dense government data tables).
- Users need to navigate to a specific page — for example, to find a stakeholder record they remember was on page 4 of an alphabetically sorted register.
- Server-side or API-driven data sets where loading all records at once is not feasible.
When not to use
- Continuous scroll interfaces such as a social activity feed — use infinite scroll or a 'Load more' button instead.
- Data sets with fewer than two pages — hiding pagination entirely is cleaner than rendering a disabled single-page control.
- Wizard or multi-step forms — use a Step Indicator component instead.
- Image galleries or media carousels — use a Carousel component with its own navigation pattern.
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
| Part | Token | Value |
|---|---|---|
| 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
- Render page number buttons as tags with an href pointing to the page URL (e.g. ?page=4) — not
- 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 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
| Key | Action |
|---|---|
| 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 attribute | When to use | Example |
|---|---|---|
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 or 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