Accessible Pagination
Pagination is one of the most common ways to break a long list of results across pages, and almost every version gets the visual part right while missing the parts a screen reader and a keyboard depend on. This guide covers the semantic markup, giving each control a name that says what it does instead of a bare number, marking the current page with aria-current, choosing links or buttons on purpose, handling the Previous and Next disabled states, and announcing the change in single-page apps. Copy-ready HTML mapped to WCAG 2.2.
Pagination Is Navigation, Not a Widget
It is easy to think of a pager as a small interactive component, something like a slider or a menu that needs a special set of ARIA roles and a keyboard model of its own. It is not. Pagination is a set of navigation controls: a row of links or buttons that each move you to a different page of the same list. There is no role="pagination", and the WAI-ARIA Authoring Practices do not define a pagination widget, because the pieces you need already exist. A named navigation landmark, a list, ordinary links or buttons, and the aria-current state do the whole job.
Because the parts are ordinary, the mistakes are ordinary too, and they come from treating the pager as decoration rather than navigation. The number 3 becomes a styled <div> that a keyboard cannot reach. The arrows become icons with no name. The active page is a colored box that a screen reader cannot see. Each of these is a small omission, and together they turn a simple navigation control into something a large group of users cannot operate at all.
The decision that shapes the rest of this guide
Before anything else, decide whether each page is a real address. If page two lives at a URL of its own, the controls should be links, so they are crawlable, bookmarkable, and work with the back button. If the results swap in place in a single-page app with no navigation, the controls should be buttons, and you owe a live region to announce the change, because nothing else will. Links go to pages; buttons change state. Almost every other choice in this guide follows from getting that one right.
The reward for getting it right is that pagination is genuinely good for accessibility, more so than the infinite scroll it often competes with. It gives structure you can bookmark, a footer you can reach, and a way to jump to a known page, which is exactly what a keyboard or screen reader user wants when they are working through a long set of results. The rest of this guide is the handful of markup choices that deliver it. There is also an interactive pagination demo on this site that lets you hear the difference between the accessible and inaccessible versions.
How Pagination Maps to WCAG 2.2
No single criterion mandates pagination, but once you build one it has to meet the criteria that apply to any set of navigation controls. The highlighted row, 2.4.4 Link Purpose, is the one pagination most characteristically fails, because a control labelled with only a number does not say where it goes. The rest of the table covers the structure, the keyboard, the current state, and, for single-page apps, the announcement.
| Criterion | Level | How it applies to pagination |
|---|---|---|
| 2.4.4 Link Purpose (In Context) | A | Each control’s name has to state what it does, so “Go to page 3,” “Previous page,” and “Next page” replace a bare number or an unlabeled arrow. |
| 1.3.1 Info and Relationships | A | The pager is a list of controls inside a named navigation region, and which page is current is exposed in the markup, not implied by styling alone. |
| 4.1.2 Name, Role, Value | A | The navigation landmark carries an accessible name, each control has a real role as a link or button, and the current page is conveyed as a state with aria-current. |
| 2.1.1 Keyboard | A | Every control is reachable and operable by keyboard, which real links and buttons give for free and div click handlers do not. |
| 1.4.1 Use of Color | A | The current page must not be distinguished by color alone; the state comes from aria-current plus a non-color cue such as an outline or weight. |
| 2.4.7 Focus Visible | AA | Each control shows a clearly visible focus indicator as the user tabs across the row of pages. |
| 2.5.8 Target Size (Minimum) | AA | Page numbers and arrows are notoriously small; each control needs at least a 24 by 24 pixel target so it can be hit reliably. |
| 4.1.3 Status Messages | AA | In a single-page app the page change and the “showing X to Y of Z” status must be announced through a live region without moving focus. |
Each criterion links to its full reference and interactive demo. The complete WCAG 2.2 criteria are one click away.
1. The Minimum Viable Accessible Pager
Start with the server-rendered case, where each page has its own URL, because it needs no JavaScript and shows the whole structure clearly. Four decisions carry almost all of the accessibility. Wrap the controls in a <nav> with an accessible name. Put the controls in a list. Make each page a link with a descriptive name. Mark the current page with aria-current="page". Here is the whole pattern:
<nav aria-label="Pagination">
<ul>
<li>
<a href="?page=1" aria-label="Go to previous page" rel="prev">
<span aria-hidden="true">‹</span>
<span class="sr-only">Previous page</span>
</a>
</li>
<li><a href="?page=1" aria-label="Go to page 1">1</a></li>
<li>
<a href="?page=2" aria-current="page" aria-label="Page 2, current page">
2
</a>
</li>
<li><a href="?page=3" aria-label="Go to page 3">3</a></li>
<li>
<a href="?page=3" aria-label="Go to next page" rel="next">
<span class="sr-only">Next page</span>
<span aria-hidden="true">›</span>
</a>
</li>
</ul>
</nav>Every decision is doing real work. The <nav aria-label="Pagination"> exposes a navigation landmark with a distinct name, so a screen reader user can find the pager and tell it apart from the main menu, which is also a navigation landmark. Write the label as Pagination, not Pagination navigation, because the role already contributes the word navigation and the longer label is announced as “Pagination navigation, navigation.”
The list gives the pager structure and an item count, so a screen reader can announce “list, five items” and let the user move control by control. An ordered list (<ol>) is the most honest choice, because the pages run in sequence, but an unordered list is common and widely accepted; the load-bearing parts are the landmark, the names, and the current-page state, not which list element you pick. Each page is a link that carries a descriptive name, which the next section covers in full, and the current page adds aria-current="page", covered in section three. That is a complete, accessible pager before a single line of script.
2. “3” Is Not a Label: Naming Every Control
The signature pagination bug is the bare number. A control whose only text is 3is announced as “3, link” or “3, button,” which tells a screen reader user nothing about what it does. Out of the visual context of a row of page numbers, a lone digit has no meaning, and that is exactly the case 2.4.4 Link Purpose is about. Every control in the pager needs a name that states the action, and there are two clean ways to provide one.
Option A: aria-label
The simplest approach puts the full name in an aria-label, while the visible content stays the short number. The visible “3” is what a sighted user reads; the label is what a screen reader announces:
<a href="?page=3" aria-label="Go to page 3">3</a>One caution: an aria-labelcompletely overrides the visible text for the accessible name, so make sure the label still contains the visible word or number. Because a sighted speech-input user says what they see, an accessible name that starts with the visible text keeps them able to say “click 3” and hit the control, which is the point of 2.5.3 Label in Name. “Go to page 3” contains the 3, so it is safe.
Option B: visually hidden text
The other approach keeps the accessible name in real text, hidden visually with an .sr-only utility, so the control reads its full name to a screen reader while the eye sees only the number. This avoids overriding the visible text at all:
<a href="?page=3">
<span class="sr-only">Go to page </span>3
</a>
<!-- The .sr-only class hides text visually but keeps it for
assistive technology. Never use display:none or
visibility:hidden here, which remove it from the
accessibility tree entirely. -->
<style>
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
</style>Previous and Next deserve special attention, because they are usually rendered as icon-only arrows, and an arrow with no text is announced as nothing more than “link” or “button.” Give each one an accessible name, “Previous page” and “Next page,” and hide the decorative glyph with aria-hidden="true" so the screen reader does not try to read the character. The pattern is exactly the one shown in section one: a visible arrow marked aria-hidden sitting beside an .sr-only name, or an aria-label on the control. Either way, the arrow carries a name that says where it goes.
3. Marking the Current Page with aria-current
A pager without a current-page marker leaves a screen reader user unable to tell which page they are on, and it is the single most common thing missing after descriptive names. aria-currentis the state that fixes it: it marks the one control in the set that represents the user’s current position. In a pager it belongs on exactly one control, the number of the page you are viewing, and a screen reader announces that control as “current page.” Put it on more than one and it stops meaning anything.
Use the value page, not the generic true. Both are valid, but aria-current has several token values for different kinds of set, and pageis the one that means “the current page in a set of pages,” which is precisely a pager. It is the same value a breadcrumb and a site menu use to mark the current page; the accessible breadcrumbs guide covers the full set of token values, if you want the reference.
A link to the page you are on, or plain text?
The current page can be a link that carries aria-current="page" or a non-interactive element, and both are accepted. Rendering it as a link keeps the row visually and behaviorally uniform, and a link to the page you are already on simply reloads it, which is harmless. Some teams instead render the current page as a plain <span> with aria-current="page", on the reasoning that a link to the current page does nothing useful. Either is fine; the two things that are not fine are leaving the current page unmarked, and marking it only with a colored background:
<!-- As a link -->
<li><a href="?page=2" aria-current="page" aria-label="Page 2, current page">2</a></li>
<!-- As plain text (also correct) -->
<li><span aria-current="page" aria-label="Page 2, current page">2</span></li>Do not let a background color be the only signal that a page is current, which would fail 1.4.1 Use of Color. Pair aria-current with a visible cue that does not rely on hue, such as an outline, an underline, or a bolder weight, and drive that styling from the attribute itself so the visual and programmatic states can never disagree:
/* Style the current page from the attribute, so the visual
cue and the announced state are always in sync. */
[aria-current="page"] {
font-weight: 700;
outline: 2px solid currentColor;
outline-offset: 2px;
}4. Links or Buttons, and the Disabled-State Trap
The choice between a link and a button is not a style preference; it reflects what the control actually does. Use a link when the page has its own URL. If page two is a real address like /articles?page=2, a link is correct: it can be bookmarked, opened in a new tab, followed by a crawler, and reached with the browser back button, all of which a button throws away. Use a button when the results change in place. In a single-page app that swaps the list without navigating, there is no new URL to link to, so a button is the honest element, and you owe the live-region announcement covered in section six. The failure modes are a link that does not navigate, a button where a shareable URL belongs, and worst of all a <div> with a click handler, which is neither.
The disabled Previous and Next trap
On the first page, Previous has nowhere to go; on the last page, Next does not either. Conveying that unavailable state is where pagers most often go wrong, and the reason is a real HTML limitation: you cannot disable an anchor. There is no disabled attribute for <a> that removes it from the tab order and marks it unavailable the way it does for a button. That leaves two correct patterns.
aria-disabled does not disable anything
aria-disabled="true" tells assistive technology that a control is unavailable, but it does not stop the control from being activated, and it does not remove it from the tab order. If you mark a link or button aria-disabled and do nothing else, a user can still click it or press Enter on it and trigger the action. Whenever you use aria-disabled, you must also prevent the action in your own handler. It is a promise you have to keep in code.
The simplest reliable pattern is to render Previous and Next as buttons and use the native disabled attribute at the ends of the range. A disabled button is removed from the tab order and announced as dimmed, with no extra code:
<!-- On page 1: Previous is a disabled button, cleanly unavailable -->
<button type="button" disabled aria-label="Previous page">
<span aria-hidden="true">‹</span>
</button>If Previous and Next have to be links, because each page is a real URL, the cleanest option is to simply not render a link when there is nowhere to go, replacing it with a non-focusable <span> that keeps the layout stable. If you would rather keep the control present and perceivable, mark it aria-disabled="true" and prevent the navigation, remembering the rule in the callout above:
<!-- Present but unavailable. The handler must block activation,
because aria-disabled alone does not. -->
<a href="?page=1"
aria-disabled="true"
aria-label="Previous page"
onclick="if (this.getAttribute('aria-disabled') === 'true') return false;">
<span aria-hidden="true">‹</span>
</a>Whichever approach you take, keep the control in a stable position so the whole row does not shift left and right as the user pages through, which is disorienting for everyone and especially for a user zoomed in or navigating by touch.
5. Long Ranges: Truncation and the Ellipsis
A hundred pages will not fit in a row, so a pager shows a window of numbers with the middle collapsed to an ellipsis, reading as 1 2 3 … 42 43 44 … 99 100. Two things have to be true for that to stay accessible: the ellipsis must not be read aloud, and the pages it hides must still be reachable.
The ellipsis is decoration. It marks a gap in the sequence, and a screen reader announcing “ellipsis” between page numbers adds only noise, so hide it with aria-hidden="true". Because it is not a control, it is not focusable and should never be a link or a button:
<nav aria-label="Pagination">
<ul>
<li><a href="?page=1" aria-label="Go to page 1">1</a></li>
<li><a href="?page=2" aria-label="Go to page 2">2</a></li>
<!-- Decoration only: not a control, hidden from screen readers -->
<li aria-hidden="true">…</li>
<li><a href="?page=42" aria-current="page" aria-label="Page 42, current page">42</a></li>
<li><a href="?page=43" aria-label="Go to page 43">43</a></li>
<li aria-hidden="true">…</li>
<li><a href="?page=100" aria-label="Go to page 100">100</a></li>
</ul>
</nav>The more important rule is about reachability. The point of the ellipsis is that not every page number is shown, but that must never mean a page becomes unreachable. Always keep the first page, the last page, the current page, and a small window of pages on either side of the current one as real controls, so a keyboard or screen reader user can move through the set without a page being stranded behind the gap. If a user needs a page that is not in the visible window, the Next control walks them toward it, and the window slides as they go. What you must not do is drop pages so aggressively that a page in the middle has no control that reaches it.
Finally, make sure the row of numbers does not force the page to scroll horizontally at 320 pixels wide, which would fail 1.4.10 Reflow. On a narrow screen, a compact window such as Previous, 42, Nextwith a “page 42 of 100” label is often clearer than a long strip of numbers, and it keeps the pager inside the viewport.
6. Announcing the Change in Single-Page Apps
When pagination is buttons that swap the results in place, a new problem appears that the server-rendered version never has: nothing reloads, so a screen reader has no event to announce, and the user may press Next and have no idea whether anything happened. This is the case 4.1.3 Status Messages exists for, and the fix is to announce the change yourself. There are three accepted ways, and you should pick one rather than combine them, because two announcements at once talk over each other.
The most common is a polite live region. Keep an always-mounted, visually hidden element with aria-live="polite" in the DOM, and update its text to the new page when the results change. It must be present before the update, not created at the moment of the change, or the first announcement is missed:
// React: an always-present polite live region announces each page.
function Pager({ page, totalPages, onGoTo }) {
return (
<>
{/* Announced politely on every page change. Always mounted. */}
<div aria-live="polite" className="sr-only">
{"Page " + page + " of " + totalPages}
</div>
<nav aria-label="Pagination">
<ul>
<li>
<button
type="button"
disabled={page === 1}
aria-label="Previous page"
onClick={() => onGoTo(page - 1)}
>
<span aria-hidden="true">‹</span>
</button>
</li>
{/* ...number buttons, current one carries aria-current="page"... */}
<li>
<button
type="button"
disabled={page === totalPages}
aria-label="Next page"
onClick={() => onGoTo(page + 1)}
>
<span aria-hidden="true">›</span>
</button>
</li>
</ul>
</nav>
</>
)
}The two alternatives are to move focus to the results heading after the page changes, which announces the heading and lands the user at the top of the new results (give the heading tabindex="-1" so it can receive focus), or to update the document <title>, which some screen readers announce on change. Moving focus is the strongest for keyboard users because it also repositions them, but it is more intrusive; the focus management guide covers the mechanics of moving focus without losing the user.
Whichever you choose, add a plain status that tells everyone where they are in the whole set, not just the page number. A short role="status"region reading “Showing 21 to 40 of 200 results” is useful to sighted users as ordinary text and is announced to screen reader users as a status message, so the same element serves both. Place it near the results, update it when the page changes, and you have given the set a sense of scale that a row of numbers alone does not convey.
7. The Alternatives: Load More and Infinite Scroll
Numbered pages are not the only way to break up a long list. Two common alternatives, a Load More button and infinite scroll, append content to the current view instead of replacing it, and they change the accessibility problem from “announce the new page” to “announce that more content arrived, and help the user find it.”
Load More: the accessible middle ground
A Load More button is the friendliest of the three patterns, because the user chooses when to load, so the moment is predictable and you own a clear place to manage focus. When the new items arrive, do two things: announce the result through a live region (“Loaded 10 more articles, 30 of 200”), and move focus to the first new item so a keyboard or screen reader user lands on the new content rather than being left at the button. The first new item needs tabindex="-1" so it can receive programmatic focus:
// After appending, move focus to the first new item.
async function handleLoadMore() {
const firstNewIndex = items.length
const more = await fetchMore()
setItems(items.concat(more))
setStatus("Loaded " + more.length + " more, " +
(items.length + more.length) + " of " + total)
// After the new items render, focus the first one.
requestAnimationFrame(() => {
itemRefs.current[firstNewIndex]?.focus()
})
}
// The first new item is programmatically focusable:
// <li tabIndex={-1} ref={el => (itemRefs.current[i] = el)}> ... </li>While the fetch is in flight, disable the button and change its text to “Loading…” so it cannot be pressed twice and so the state is announced. This is the same discipline as the page-change live region above, and the interactive demo shows both the Load More and infinite-scroll versions running.
Infinite scroll: use it carefully, and provide an alternative
Infinite scroll loads more content automatically as the user nears the bottom, and it carries the most accessibility risk of the three patterns. A screen reader user is not told that new items appeared unless you announce it through a live region. A keyboard user can be unable to reach the footer at all, because it keeps moving further down as new content loads. And no one can bookmark a position or return to where they were. If you adopt it anyway, announce the loading and loaded states politely, make sure a keyboard user can Tab past the feed to whatever follows it, and provide an explicit alternative, whether that is a Load More button, a “view all” link, or classic pagination. The pattern that appends articles as you scroll has its own ARIA support in role="feed", but for most lists a Load More button is simpler and kinder. For the focus and live-region details these patterns share, see the focus management guide and the 4.1.3 Status Messages reference.
8. Testing a Pager
Keyboard
Tab through the pager. Every control, the numbers and the Previous and Next arrows, should take focus in order, show a clearly visible focus indicator, and activate with Enter, plus Space if it is a button. A disabled Previous or Next should be skipped by Tab, not focusable and dead. If any control is unreachable by keyboard, it is almost certainly a <div> or <span> with a click handler rather than a real link or button.
Screen reader
This is the test that catches the real bugs. Open the landmarks list and confirm the pager appears as “Pagination,” distinct from the main navigation. Move into it and listen: each control should announce a name that states its action (“Go to page 3,” “Next page”), not a bare number or a lone “button,” and the current page should announce as “current page.” In a single-page app, activate Next and confirm you hear “Page 3 of 10” or land on the results heading. If you hear a number with no context, your names are missing; if you never hear “current page,” the aria-current is not there. Verify with more than one reader if you can, using the NVDA and VoiceOver guides.
Target size and reflow
Check that each control is at least a 24 by 24 pixel target and that adjacent pages are spaced so they are not mis-tapped, which matters most on touch. Then narrow the viewport to 320 pixels or zoom to 400 percent and confirm the pager reflows instead of forcing horizontal scrolling, collapsing to a compact window if it needs to.
What tools catch, and what they do not
Automated checkers such as axe and WAVE will flag a navigation landmark with no accessible name, a control with no name at all, and a target that is too small. What they cannot judge is whether the trail of names is right: whether the current page is the one actually marked, whether the announcement fires when the page changes, or whether a truncated page is still reachable. As the automated versus manual testing guide puts it, the machine gets you to valid markup and a person decides whether it is correct. For where this fits in a full review, see the accessibility audit guide.
Common Pagination Mistakes & How to Fix Them
These are the errors that turn up most in real pagination audits. Most come back to two habits: building the controls out of the wrong elements, and labelling them with a number or an arrow instead of a name that says what they do.
| Anti-pattern | Why it fails | The fix |
|---|---|---|
| The page controls are <div>s with onClick handlers. | Divs are not focusable or operable by keyboard and expose no role, so a keyboard user cannot reach them and a screen reader announces nothing useful (fails 2.1.1 and 4.1.2). | Use a real <a> for links to pages, or a <button> for in-place changes; never a div or span with a click handler. |
| The pagination has no landmark or accessible name. | There is no navigation region to find, so a screen reader user cannot jump to the pager or tell it apart from other navigation (weakens 1.3.1 and 4.1.2). | Wrap the controls in <nav aria-label="Pagination">, giving the landmark a distinct name without the word navigation in it. |
| Page controls are labelled with only the bare number, like "3". | A screen reader announces "3, link" with no indication that it changes the page, so the purpose is unclear out of context (fails 2.4.4). | Give each control a name that states the action, such as "Go to page 3", via aria-label or visually hidden text. |
| Previous and Next are icon-only with no accessible name. | The control announces as just "button" or "link", so its purpose is lost to anyone who cannot see the arrow (fails 2.4.4 and 4.1.2). | Give the icon control the accessible name "Previous page" or "Next page", and hide the decorative glyph with aria-hidden. |
| The current page is shown only with a colored background. | A user who cannot perceive the color, and every screen reader user, gets no cue about which page is current (fails 1.4.1 and weakens 1.3.1). | Mark it with aria-current="page" plus a visible non-color cue, and use [aria-current] as the styling hook so the two cannot disagree. |
| In a single-page app the results swap with no announcement. | Without a full page load a screen reader user is not told the content changed, so they do not know the page turned (fails 4.1.3). | Announce "Page 3 of 10" through a polite live region, or move focus to the results heading, or update the document title. |
| Tiny number and arrow targets sit a few pixels apart. | Controls smaller than 24 by 24 CSS pixels are hard to hit for people with motor differences and on touch screens, and crowded targets get mis-tapped (fails 2.5.8). | Give each control at least a 24 by 24 pixel target, 44 is better, with enough spacing that adjacent pages are not mistaken for one another. |
The Accessible Pagination Checklist
- Wrapped in a named landmark. The pager sits inside
<nav aria-label="Pagination">, so it is a distinct navigation landmark, without the word navigation in the label. - A list of controls. The controls are a list, one
<li>each, so assistive technology can present them as a counted set. - Links or buttons, chosen on purpose. Links when each page has a real URL; buttons when the results change in place; never a div or span with a click handler.
- Every control is named. “Go to page 3,” “Previous page,” and “Next page” replace bare numbers and unlabeled arrows, via
aria-labelor visually hidden text. - The current page is marked. Exactly one control carries
aria-current="page", with a visible non-color cue, never a colored background alone. - Previous and Next handle their ends. At the first and last page they are a disabled button, or an aria-disabled control whose action is prevented, held in a stable position.
- The ellipsis is hidden, pages stay reachable. The ellipsis carries
aria-hidden="true", and the first, last, current, and surrounding pages are always real controls. - Focus is visible and targets are big enough. Each control shows a clear focus indicator and is at least a 24 by 24 pixel target.
- Single-page changes are announced. A live region says “Page 3 of 10,” or focus moves to the results heading, plus a “showing X to Y of Z” status.
- Verified with a screen reader. The Pagination landmark is findable, each control’s name states its action, and the current page announces as current.
Pagination Is One of a Family of Navigation Patterns
A pager is a named navigation landmark built on aria-current, the same foundation as a breadcrumb trail. See its sibling pattern, and try the live version of every pagination pattern in the interactive demo.
Frequently Asked Questions
Should pagination use links or buttons?▾
It depends on whether each page is a real address. When the site is server-rendered and page two lives at a URL like /articles?page=2, use links, because links are crawlable, bookmarkable, open in a new tab, and work with the browser back button. When you have a single-page app that swaps the results in place without a full navigation, use buttons, and pair them with a live region so the change is announced. The choices that fail are a div or span with a click handler, which no keyboard user can reach; a link that does not actually navigate anywhere; and a button where the page ought to have a shareable URL. Decide by asking whether the page has an address of its own.
What accessible name should a page number control have?▾
Not the bare number. A control that reads only "3" is announced as "3, link" or "3, button", which gives a screen reader user no clue that activating it changes the page. Give each control a name that states the action, such as "Go to page 3", and let the current one announce as the current page through aria-current. Supply the name with aria-label, or with visually hidden text inside the control, for example a span with class sr-only reading "Go to page " before the visible "3". Previous and Next controls, especially icon-only ones, need names too: "Previous page" and "Next page".
How do I mark the current page in pagination?▾
Put aria-current="page" on the single control that represents the page you are on, and on no other. A screen reader then announces that control as the current page, which is how a non-visual user knows where they are in the set. Do not signal the current page with a colored background or bold weight alone, because a user who cannot perceive the color and every screen reader user would get nothing. Pair aria-current with a visible non-color cue, and you can use the attribute itself as the CSS hook, styling [aria-current="page"], so the visual state and the programmatic state can never drift apart.
How should disabled Previous and Next controls behave?▾
On the first page Previous has nowhere to go, and on the last page Next does not either, so those controls need an unavailable state. You cannot truly disable an anchor element, so the accessible options are to render Previous and Next as buttons and use the native disabled attribute, which removes them from the tab order and announces them as unavailable, or to keep them perceivable with aria-disabled="true" and then prevent the action in your own handler, because aria-disabled communicates the state but does not stop activation on its own. Whichever you choose, keep the control in a stable position so the row does not jump between pages; rendering Previous and Next as buttons that disable at the ends is the simplest reliable choice.
Is the ellipsis in "1 2 3 ... 10" an accessibility problem?▾
The ellipsis is decoration that marks a gap in the range of page numbers, and it should be hidden from assistive technology with aria-hidden="true" so a screen reader does not read "ellipsis" between the numbers. What actually matters is that the pages the gap stands for remain reachable. Keep the first page, the last page, the current page, and a small window of pages around the current one as real controls, so a keyboard or screen reader user can always reach the pages they need even though not every number is shown at once. The ellipsis is only a visual shorthand; it must never be the reason a page becomes unreachable.
How do I announce a page change in a single-page app?▾
In a single-page app the results can change while the browser performs no full page load, so a screen reader has nothing to announce and the user may not realize the page turned. Fix this by announcing the change yourself through an always-present polite live region that updates to read "Page 3 of 10", or by moving focus to the results heading, or by updating the document title. Choose one of these; doing two at once tends to double-announce. It also helps everyone to expose a short status such as "Showing 21 to 40 of 200" so users know where they are within the whole set, not just which page number is active.
Is pagination better than infinite scroll for accessibility?▾
Usually, yes. Numbered pagination gives a clear structure, pages you can bookmark, a footer you can actually reach, and a way to jump to a specific page, all of which infinite scroll takes away. Infinite scroll adds real problems: screen reader users are not told that new content loaded unless you announce it, keyboard users can struggle to reach a footer that keeps moving further down, and no one can bookmark or return to a position. If you do use infinite scroll, announce the loading and loaded states through a live region and offer an alternative such as a Load More button or classic pagination. A Load More button is a good middle ground, because it is user-initiated, easy to announce, and a natural place to move focus to the first new item.
Do I still need rel="next" and rel="prev"?▾
They are optional and low-stakes today. Google confirmed in 2019 that it no longer uses rel="next" and rel="prev" as an indexing signal, so they are not required for search, and they were never an accessibility feature, because screen readers do not depend on them. They do no harm and a handful of tools still read them, but the accessibility of a pager comes from the visible, named, keyboard-operable controls and the current-page state, not from these link relations. Spend your effort on the nav landmark, the accessible names, and the aria-current state rather than on the link header.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences