Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Pagination

Pagination is a set of page numbers that lets someone jump between the pages of a longer list, such as search results or a data table, with first, previous, next, and last controls, and collapsing gaps once there are too many pages to show at once.

It's fully controlled and framework-agnostic by design: every control is a plain button, never a real link, so there's no native navigation for an SPA router to intercept in the first place. Activating one just requests the page, and pagination never updates its own current page in response. Applying the change, such as setting the page back, fetching the new page's data, or updating a query string through your router, stays entirely your own responsibility (see "SPA and router integration" in the Examples tab).

Dependencies

eun-icon eun-select · if pageSizeOptions is set
Overview API Examples Accessibility

When to use

Add pagination once a list is too long to show all at once and you'd rather split it into discrete pages than load everything, or scroll endlessly, such as search results, a data table, or an admin listing. Prefer infinite list instead when "keep loading more as you scroll" fits the content better than discrete, addressable pages. Reach for pagination when someone benefits from jumping straight to a specific page, sharing a link to it, or seeing roughly how much content there is in total. See Alternatives below for a side by side comparison.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/pagination.js";
<eun-pagination
  page="1"
  total-pages="12"
  aria-label="Pagination"
></eun-pagination>

<script>
  document
    .querySelector("eun-pagination")
    .addEventListener("eunpagechange", (event) => {
      // Apply the change yourself : fetch the new page, update the URL, ...
      event.target.page = event.page;
    });
</script>

Importing the file registers <eun-pagination> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component. page is entirely yours to own: eun-pagination only tells you which page was requested through eunpagechange, it never sets page back itself (see the API tab).

Alternatives

You want .. Prefers To keep loading more as someone scrolls, instead of discrete, addressable pages eun-infinite-list

Guidance

  • Set page back to the requested value once you've actually applied it (fetched the data, updated the URL, ...), not optimistically before that, so the UI never shows a page whose content hasn't loaded yet
  • Set disabled while the requested page's data is loading, so a second click can't race the first (see the Examples tab)
  • Keep totalPages in sync with your actual data, since eun-pagination has no way to know it on its own
  • Leave siblingCount/boundaryCount at their defaults unless you have a specific reason to show more or fewer page numbers, since the defaults already match common pagination conventions
  • Recompute totalPages and reset page to 1 when applying a eunpagesizechange, since the old page position no longer means anything once the page size changes (see the Examples tab)
  • Expecting page to update itself after a click: it never does, on purpose (see "Fully controlled" above)
  • Using it for a handful of items that fit on one page: a two-page list rarely earns a full pagination control
  • Reaching for it when "keep loading more" fits better: see Alternatives above

Live testing

Properties

page and totalPages are the two you'll always set. Everything else tunes the collapsed-page-range algorithm, hides controls, shows a visible label next to First/Previous/Next/Last (showLabels), relabels controls for another language, or adds a page-size selector (pageSizeOptions, see the Examples tab).

Pagination <eun-pagination>

Attributes

NameTypeDefaultDescription
total-pagesnumber1The total number of pages
sibling-countnumber1How many page numbers to show on each side of the current page before collapsing the rest into an ellipsis gap
boundary-countnumber1How many page numbers to always show at the very start and end
hide-prev-nextbooleanfalseHides the previous and next controls
hide-first-lastbooleanfalseHides the first and last controls
show-labelsbooleanfalseShows a visible text label next to the first, previous, next, and last icons, instead of icon-only controls
page-sizenumberThe current page size, in items per page. Only meaningful once pageSizeOptions is also set
page-size-optionsArray<number>[]The selectable page sizes. Setting this renders a page-size selector alongside the page controls. Left empty, the default, no selector is rendered at all
page-size-labelstring'Items per page'The label for the page-size selector
aria-label-previousstring'Previous page'The accessible label of the previous control
aria-label-nextstring'Next page'The accessible label of the next control
aria-label-firststring'First page'The accessible label of the first control
aria-label-laststring'Last page'The accessible label of the last control
aria-label-pagestring'Page'The accessible label prefix applied to every page number control, such as Page 3
aria-labelstring'Pagination'The accessible label for the navigation landmark
pagenumber1The current page, starting at 1
disabledbooleanfalseDisables every control, such as while the current page's data is loading

Import the exact TypeScript type behind any property above, see Types.

Events

NameTypeDescription
eunpagechangePageChangeEventFired with the requested page whenever a control is activated. Page is never updated internally
eunpagesizechangePageSizeChangeEventFired with the chosen value whenever the page-size selector changes. PageSize is never updated internally

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--pagination-gapSets the gap between controls
--pagination-item-sizeSets the width and height of every control
--pagination-border-radiusSets the corner radius of every control
--pagination-item-colorSets the text and icon color of non-current controls
--pagination-item-color-hoverSets the text and icon color of non-current controls on hover
--pagination-item-background-hoverSets the background color of page-number controls on hover
--pagination-control-backgroundSets the background color of the first, previous, next, and last controls, distinguishing them from page numbers
--pagination-control-background-hoverSets the background color of the first, previous, next, and last controls on hover
--pagination-current-backgroundSets the background color of the current page
--pagination-current-colorSets the text color of the current page
--pagination-disabled-colorSets the text and icon color of disabled controls
--pagination-ellipsis-colorSets the color of the ellipsis
--pagination-focus-outline-colorSets the color of a control's focus outline
--pagination-font-sizeSets the font size of every control
--pagination-transition-durationSets the duration of the hover color and background transition

Basic

Few enough pages that every number fits, so no gap is needed.

<eun-pagination
  page="2"
  total-pages="5"
  aria-label="Pagination"
></eun-pagination>

Collapsed range

Past a certain page count, the middle collapses into gaps on one or both sides of the current page, always keeping the first/last boundaryCount pages and the current page's siblingCount neighbors visible.

<eun-pagination
  page="10"
  total-pages="30"
  aria-label="Pagination"
></eun-pagination>

Custom sibling and boundary count

<eun-pagination
  page="10"
  total-pages="30"
  sibling-count="2"
  boundary-count="2"
  aria-label="Pagination"
></eun-pagination>

Hiding controls

Hide First/Last when jumping straight to either end rarely matters, or Previous/Next when the page numbers alone are enough.

<eun-pagination
  page="10"
  total-pages="30"
  hide-first-last
  aria-label="Pagination"
></eun-pagination>

Labeled controls

showLabels adds a visible text label next to the First/Previous/Next/Last icons, the same strings as their aria-label-*, for a more explicit, less icon-reliant control. The icon always points away from its label (see the Accessibility tab).

<eun-pagination
  page="4"
  total-pages="12"
  show-labels
  aria-label="Pagination"
></eun-pagination>

Disabled while loading

Set disabled while the requested page's data is in flight, so a second click can't race the first. See "SPA and router integration" below for the full pattern.

<eun-pagination
  page="4"
  total-pages="12"
  disabled
  aria-label="Pagination"
></eun-pagination>

Custom

Override the --pagination-* CSS variables, listed in full in the API tab.

<eun-pagination
  class="custom-pagination"
  page="4"
  total-pages="12"
  aria-label="Pagination"
></eun-pagination>
.custom-pagination {
  --pagination-current-background: #be185d;
  --pagination-item-background-hover: #fce7f3;
  --pagination-border-radius: 999px;
}

Page-size selector

Setting pageSizeOptions renders a eun-select-powered page-size selector alongside the page controls, labeled with pageSizeLabel. Left unset (the default), no selector is rendered at all, since eun-pagination has no page size of its own to offer choices for. Like eunpagechange, eunpagesizechange is a pure request: pageSize is never updated internally, and neither is page/totalPages, since changing how many items fit per page almost always changes the total page count too, which only you know how to recompute.

import "@eunomia/elements/pagination.js";
import "@eunomia/elements/select.js"; // powers the page-size selector
<eun-pagination
  page="1"
  total-pages="10"
  page-size="10"
  page-size-options="[10, 25, 50]"
  aria-label="Pagination"
></eun-pagination>
const pagination = document.querySelector("eun-pagination");

pagination.addEventListener("eunpagesizechange", (event) => {
  const totalItems = 97; // however you know your actual total is
  pagination.pageSize = event.pageSize;
  pagination.totalPages = Math.ceil(totalItems / event.pageSize);
  pagination.page = 1; // the old page position no longer means anything
});

SPA and router integration

eun-pagination never touches the URL or your router itself. It only tells you which page was requested. The pattern is always the same, regardless of framework: listen for eunpagechange, apply the change however fits your app, then set page back once it's actually done.

const pagination = document.querySelector("eun-pagination");

pagination.addEventListener("eunpagechange", async (event) => {
  pagination.disabled = true; // block a second click while this one's in flight
  await goToPage(event.page); // your own router push / fetch / state update
  pagination.page = event.page;
  pagination.disabled = false;
});

Keyboard interactions

eun-pagination adds no custom keyboard handling of its own, since every control is a native <button> (or, for the current page, plain non-interactive text), so standard button/focus behavior already applies. Unlike a listbox or tablist, there's no dedicated "pagination" pattern in the WAI-ARIA APG requiring roving tabindex or arrow-key navigation between controls, so this deliberately stays a plain sequence of independent buttons rather than a composite widget:

Key Action
Tab / Shift+Tab Moves focus between controls, in visual order
Enter / Space Activates the focused control

Aria attributes and rules

  • Rendered as <nav aria-label="Pagination"> wrapping a <ul role="list">, following the same landmark-plus-list structure as eun-breadcrumb. Override aria-label for other languages/contexts.
  • role="list" is set explicitly despite <ul> having it implicitly: Safari drops the implicit role once list-style: none is applied (needed here for the visual flex layout), which would otherwise silently skip the list for VoiceOver users.
  • The current page renders as a non-interactive <span aria-current="page">, never a button, mirroring eun-breadcrumb's current step, since activating the page someone's already on is a confusing, redundant control.
  • First/Previous/Next/Last and every page-number control carry an explicit aria-label (aria-label-first/aria-label-previous/aria-label-next/aria-label-last/aria-label-page), since a lone chevron icon or a bare digit has no accessible name on its own otherwise.
  • Unreachable controls (Previous/First on page 1, Next/Last on the last page, or every control while disabled is set) use the native disabled attribute, not aria-disabled, so they're properly excluded from the tab order and announced as unavailable by assistive technology, not just visually dimmed.
  • gaps are rendered as <span aria-hidden="true">…</span>: a purely visual indicator of skipped pages, not a control. There's nothing to activate, so it's excluded from the accessibility tree entirely rather than being a dead, focusable stop.
  • While showLabels is set, First/Previous/Next/Last drop their aria-label entirely rather than keeping it alongside the now-visible text, so the button's accessible name comes from its content instead, avoiding two competing sources of truth for the same name, and keeping the visible label and accessible name identical (WCAG 2.5.3, Label in Name). eun-icon stays aria-hidden either way, so it never contributes to that name.
  • The page-size selector (pageSizeOptions, see the Examples tab) is a eun-select field, given its own accessible name through pageSizeLabel, so no extra ARIA is needed on top of what it already provides on its own (see the Select component).

No real links, on purpose

Every control is a <button>, never an <a href>. Unlike eun-button/ eun-link/eun-breadcrumb-item, eun-pagination never performs (or lets a router intercept) a native navigation at all. This is deliberate: pages are almost always fetched or rendered client-side rather than served from a distinct URL per page, so there's no href to meaningfully build in the first place. If your pages are individually linkable (e.g. server-rendered ?page=N results), keep the URL in sync yourself from eunpagechange (see "SPA and router integration" in the Examples tab) rather than expecting eun-pagination to manage it.

Reference links

WAI-ARIA Authoring Practices: Patterns
WAI Web Accessibility Tutorials: Labeling Controls