Skip to content

Accessibility specialists

  • About
  • Services
  • Blog
  • Resources
Email us
  1. Home
  2. Resources
  3. Accessible components library
  4. Pagination

Pages in this section

Components

  • Accordion
  • Breadcrumbs
  • Dialog
  • Disclosure
  • Pagination
    • Accessibility requirements
    • Test scenarios
  • Tabs
  • Text input
  • Textarea

More information

  • Accessibility requirements
  • Compatibility notes
  • Glossary
  • Test steps
  • View the project on GitHub

Pagination

Pagination is a type of navigation menu, typically displayed as a row of numbered links, that helps users quickly navigate content that is split into sequential pages.

The term "pagination" comes from the verb to "paginate", which means to break content over numbered pages. It was typically used in publishing and book binding, but has made its way onto the web.

Pagination patterns often include:

  • Numbered links that take users directly to a page
  • A visual indicator for the current page
  • Truncation when there are too many pages to show them all
  • "Previous" and "Next" links to move the current page forwards or backwards

Example

Open this example in a new tab
  • HTML
  • CSS
<nav class="tl-pagination" aria-label="Pagination">
  <a href="?page=5" aria-label="Previous page">&lt;&nbsp;Previous</a>

  <ol role="list">
    <li>
      <a href="?page=1">1</a>
    </li>

    <li>
      <span aria-hidden="true">⋯</span>
      <span class="visually-hidden">Ellipsis</span>
    </li>

    <li>
      <a href="?page=4">4</a>
    </li>

    <li>
      <a href="?page=5">5</a>
    </li>

    <li>
      <a href="?page=6" aria-current="page">6</a>
    </li>

    <li>
      <a href="?page=7">7</a>
    </li>

    <li>
      <a href="?page=8">8</a>
    </li>

    <li>
      <a href="?page=9">9</a>
    </li>

    <li>
      <span aria-hidden="true">⋯</span>
      <span class="visually-hidden">Ellipsis</span>
    </li>

    <li>
      <a href="?page=13">13</a>
    </li>
  </ol>

  <a href="?page=7" aria-label="Next page">Next&nbsp;&gt;</a>
</nav>
/* -------------------------------------------------------------------------- */
/* Pagination                                                                 */
/* -------------------------------------------------------------------------- */

.tl-pagination {
  --colour-link-text: var(--tl-colour-text-link, #7f238b);
  --colour-link-text-hover: var(
    --tl-colour-text-link-hover,
    var(--tl-colour-text-link, #420065)
  );
  --colour-link-text-current: var(--tl-colour-page, #ffffff);
  --colour-link-background-current: var(--tl-colour-primary, #420065);
  --colour-ellipsis: var(--tl-colour-text, #222222);
  --colour-focus-highlight: var(--tl-colour-highlight-focus, #7f238b);
  --colour-focus-contrasting: var(--tl-colour-contrasting-focus, #ffffff);

  /* Must match the maximum number of items rendered in the list */
  --columns: 10;
}

nav.tl-pagination {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  max-inline-size: 50ch;
}

nav.tl-pagination ol {
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(24px, 44px));
  grid-row: 1;
  grid-column: 1 / -1;
  justify-content: space-between;
  padding: 0;
  margin: 0;
  list-style: none;
}

/* The hidden text beside each ellipsis keeps the sizing of its utility class,
   so the span rules leave it alone */
nav.tl-pagination a,
nav.tl-pagination span:not(.visually-hidden) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 24px;
  min-block-size: 44px;
}

nav.tl-pagination ol a,
nav.tl-pagination ol span:not(.visually-hidden) {
  inline-size: 100%;
}

nav.tl-pagination a {
  color: var(--colour-link-text);
}

nav.tl-pagination span {
  color: var(--colour-ellipsis);
}

/* Previous link */
nav.tl-pagination > a:first-child {
  grid-row: 2;
  grid-column: 1;
  justify-self: start;
}

/* Next link */
nav.tl-pagination > ol + a {
  grid-row: 2;
  grid-column: 2;
  justify-self: end;
}

/* State styles */
nav.tl-pagination a:visited {
  color: var(--colour-link-text);
}

nav.tl-pagination a[aria-current="page"] {
  font-weight: bold;
  color: var(--colour-link-text-current);
  background-color: var(--colour-link-background-current);
}

nav.tl-pagination a:focus-visible {
  outline: 3px solid var(--colour-focus-highlight);
  outline-offset: 3px;
  box-shadow: 0 0 0 6px var(--colour-focus-contrasting);
}

nav.tl-pagination a:hover,
nav.tl-pagination a:visited:hover {
  color: var(--colour-link-text-hover);
}

nav.tl-pagination a[aria-current="page"]:hover {
  color: var(--colour-link-text-current);
}

/* Background colours are removed in forced colours mode, so use a border to identify the current page */
@media (forced-colors: active) {
  nav.tl-pagination a[aria-current="page"] {
    border: 2px solid currentcolor;
  }
}
  • HTML
  • CSS
<nav class="tl-pagination" aria-label="Pagination">
  <ol role="list">
    <li>
      <a href="?page=1">1</a>
    </li>

    <li>
      <a href="?page=2">2</a>
    </li>

    <li>
      <a href="?page=3" aria-current="page">3</a>
    </li>

    <li>
      <a href="?page=4">4</a>
    </li>

    <li>
      <a href="?page=5">5</a>
    </li>
  </ol>
</nav>
/* -------------------------------------------------------------------------- */
/* Pagination                                                                 */
/* -------------------------------------------------------------------------- */

.tl-pagination {
  --colour-link-text: var(--tl-colour-text-link, #7f238b);
  --colour-link-text-hover: var(
    --tl-colour-text-link-hover,
    var(--tl-colour-text-link, #420065)
  );
  --colour-link-text-current: var(--tl-colour-page, #ffffff);
  --colour-link-background-current: var(--tl-colour-primary, #420065);
  --colour-ellipsis: var(--tl-colour-text, #222222);
  --colour-focus-highlight: var(--tl-colour-highlight-focus, #7f238b);
  --colour-focus-contrasting: var(--tl-colour-contrasting-focus, #ffffff);

  /* Must match the maximum number of items rendered in the list */
  --columns: 10;
}

nav.tl-pagination {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  max-inline-size: 50ch;
}

nav.tl-pagination ol {
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(24px, 44px));
  grid-row: 1;
  grid-column: 1 / -1;
  justify-content: space-between;
  padding: 0;
  margin: 0;
  list-style: none;
}

/* The hidden text beside each ellipsis keeps the sizing of its utility class,
   so the span rules leave it alone */
nav.tl-pagination a,
nav.tl-pagination span:not(.visually-hidden) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 24px;
  min-block-size: 44px;
}

nav.tl-pagination ol a,
nav.tl-pagination ol span:not(.visually-hidden) {
  inline-size: 100%;
}

nav.tl-pagination a {
  color: var(--colour-link-text);
}

nav.tl-pagination span {
  color: var(--colour-ellipsis);
}

/* Previous link */
nav.tl-pagination > a:first-child {
  grid-row: 2;
  grid-column: 1;
  justify-self: start;
}

/* Next link */
nav.tl-pagination > ol + a {
  grid-row: 2;
  grid-column: 2;
  justify-self: end;
}

/* State styles */
nav.tl-pagination a:visited {
  color: var(--colour-link-text);
}

nav.tl-pagination a[aria-current="page"] {
  font-weight: bold;
  color: var(--colour-link-text-current);
  background-color: var(--colour-link-background-current);
}

nav.tl-pagination a:focus-visible {
  outline: 3px solid var(--colour-focus-highlight);
  outline-offset: 3px;
  box-shadow: 0 0 0 6px var(--colour-focus-contrasting);
}

nav.tl-pagination a:hover,
nav.tl-pagination a:visited:hover {
  color: var(--colour-link-text-hover);
}

nav.tl-pagination a[aria-current="page"]:hover {
  color: var(--colour-link-text-current);
}

/* Background colours are removed in forced colours mode, so use a border to identify the current page */
@media (forced-colors: active) {
  nav.tl-pagination a[aria-current="page"] {
    border: 2px solid currentcolor;
  }
}

What problems it solves

The pagination component divides content across multiple pages. It offers a user-friendly way to present large datasets in manageable chunks, reducing cognitive load. It also helps with performance by improving load speeds and reducing rendering lag.

When to use it

Do use the pagination component if:

  • You need to display large datasets such as blog post listings, e-commerce products or search results
  • The user has enough time to browse the dataset
  • The dataset is not part of a linear flow or step-by-step process

When not to use it

Do not use the pagination component if:

  • A manageable amount of content can be displayed on a single screen
  • Finding the information in the dataset is time sensitive
  • The dataset is part of a linear workflow or step-by-step process, as it can cause distractions and disrupt the intended flow

Where to use it

Pagination components should be placed below the content they control. This creates a natural flow where the user browses content from top to bottom, then selects another page to display more content.

How it works

The pagination is basically just an ordered list of links with some additional semantics. The list uses a native <ol> element, but also includes the attribute role="list", due to Safari removing list semantics when the list marker is missing in the compatibility notes.

When there are more than ten pages, the pagination component will:

  • Truncate one or more links
  • Use the current page as the centre point for the truncation window
  • Never truncate the first or last page
  • Show "Previous" and/or "Next" links

Truncation

The truncation window is a sliding window which can only contain ten items. The first and last pages are always at the start and end, and the middle eight items are calculated forwards and backwards from the current page.

An ellipsis appears in place of the links which fall outside of the truncation window. It can appear in slot two, after the first page, and/or in slot nine, before the last page. This keeps the pages immediately before and after the current page visible.

Screen readers treat the ellipsis character as punctuation, and some, such as NVDA at its default punctuation level and TalkBack, do not announce it at all. So the character is hidden from screen readers, and visually hidden text alongside it reads "Ellipsis", which every screen reader announces.

Examples:

  • [1] 2 3 4 5 6 7 8 … 13
  • 1 … 4 5 [6] 7 8 9 … 13
  • 1 … 6 7 8 9 10 11 12 [13]

Previous and Next links

The Previous link:

  • Is not rendered when on the first page
  • Links to the page immediately before the current page

The Next link:

  • Is not rendered when on the last page
  • Links to the page immediately after the current page

Assistive technology compatibility

This component has been tested with the following browsers and assistive technologies.

Assistive technology Result Notes
Keyboard No issues No notes
Voice Access/Chrome 154.0.8/Windows 11 No issues No notes
JAWS 2026.2603.86/Chrome 154.0.8/Windows 11 No issues No notes
NVDA 2026.1.1/Firefox 157.0/Windows 11 No issues NVDA does not announce the elipses, but announces the visually hidden word as the workaround.
VoiceOver/Safari macOS Tahoe 26.6 No issues No notes
VoiceOver/Safari/iOS 26.0 No issues No notes
TalkBack/Chrome 154.0.8/Android 17 No issues TalkBack does not announce the elipses, but announces the visually hidden word as the workaround.
Windows Contrast themes No issues No notes
Windows Magnifier and/or Apple Zoom No issues No notes
Browser zoom No issues No notes
prefers-reduced-motion Not applicable There are no animations to reduce

Last updated: 13 October 2026.

Back to top

We like to listen

Wherever you are in your accessibility journey, get in touch if you have a project or idea.

hello@tetralogical.com
Colour scheme

60 Windsor Avenue, London, SW19 2RR, UK +44 (0)20 8895 6768 hello@tetralogical.com

© TetraLogical Services Ltd. All Rights Reserved. Company number 12951928.

Privacy policy • Accessibility statement • Code of ethics