Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Make pagination accessible

Pagination controls are accessible with proper ARIA labels, keyboard navigation, and current page indication.

Utilities
Quick take
Typical fix time 20 min
  • Use nav element with aria-label='Pagination'
  • Mark current page with aria-current='page'
  • Provide clear labels for previous/next buttons
  • Announce page changes to screen readers
Why it matters: Without proper markup, screen reader users cannot understand which page they're on or navigate efficiently through paginated content.

Rule Details

Accessible pagination helps users understand their current position and navigate through paged content.

Code Example

<nav aria-label="Pagination" class="pagination">
  <ul>
    <li>
      <a href="?page=1" aria-label="Go to previous page">
        Previous
      </a>
    </li>
    <li>
      <a href="?page=1" aria-label="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="Page 3">3</a>
    </li>
    <li>
      <span aria-hidden="true">...</span>
    </li>
    <li>
      <a href="?page=10" aria-label="Page 10">10</a>
    </li>
    <li>
      <a href="?page=3" aria-label="Go to next page">
        Next
      </a>
    </li>
  </ul>
</nav>

Why It Matters

Without proper markup, screen reader users cannot understand which page they're on or navigate efficiently through paginated content.

Accessibility Requirements

RequirementImplementation
Container<nav> with aria-label="Pagination"
Current pagearia-current="page"
Page labelsDescriptive text for screen readers
Keyboard accessAll controls focusable
State changesAnnounce via live regions

React Pagination Component

interface PaginationProps {
  currentPage: number
  totalPages: number
  onPageChange: (page: number) => void
  siblingCount?: number
}
 
export function Pagination({
  currentPage,
  totalPages,
  onPageChange,
  siblingCount = 1
}: PaginationProps) {
  const [announcement, setAnnouncement] = useState('')
 
  const getPageNumbers = () => {
    const pages: (number | 'ellipsis')[] = []
    const leftSibling = Math.max(currentPage - siblingCount, 1)
    const rightSibling = Math.min(currentPage + siblingCount, totalPages)
 
    // Always show first page
    if (leftSibling > 1) {
      pages.push(1)
      if (leftSibling > 2) pages.push('ellipsis')
    }
 
    // Show sibling pages
    for (let i = leftSibling; i <= rightSibling; i++) {
      pages.push(i)
    }
 
    // Always show last page
    if (rightSibling < totalPages) {
      if (rightSibling < totalPages - 1) pages.push('ellipsis')
      pages.push(totalPages)
    }
 
    return pages
  }
 
  const handlePageChange = (page: number) => {
    onPageChange(page)
    setAnnouncement(`Page ${page} of ${totalPages}`)
  }
 
  const pageNumbers = getPageNumbers()
 
  return (
    <>
      <nav aria-label="Pagination" className="pagination">
        <ul className="pagination__list">
          {/* Previous button */}
          <li>
            <button
              type="button"
              onClick={() => handlePageChange(currentPage - 1)}
              disabled={currentPage === 1}
              aria-label="Go to previous page"
              className="pagination__button"
            >
              ← Previous
            </button>
          </li>
 
          {/* Page numbers */}
          {pageNumbers.map((page, index) =>
            page === 'ellipsis' ? (
              <li key={`ellipsis-${index}`} aria-hidden="true">
                <span className="pagination__ellipsis">...</span>
              </li>
            ) : (
              <li key={page}>
                <button
                  type="button"
                  onClick={() => handlePageChange(page)}
                  aria-current={page === currentPage ? 'page' : undefined}
                  aria-label={
                    page === currentPage
                      ? `Page ${page}, current page`
                      : `Go to page ${page}`
                  }
                  className={`pagination__button ${
                    page === currentPage ? 'pagination__button--current' : ''
                  }`}
                >
                  {page}
                </button>
              </li>
            )
          )}
 
          {/* Next button */}
          <li>
            <button
              type="button"
              onClick={() => handlePageChange(currentPage + 1)}
              disabled={currentPage === totalPages}
              aria-label="Go to next page"
              className="pagination__button"
            >
              Next →
            </button>
          </li>
        </ul>
      </nav>
 
      {/* Live region for announcements */}
      <div aria-live="polite" aria-atomic="true" className="sr-only">
        {announcement}
      </div>
    </>
  )
}

With Page Size Selector

interface PaginationWithSizeProps extends PaginationProps {
  pageSize: number
  totalItems: number
  onPageSizeChange: (size: number) => void
}
 
export function PaginationWithSize({
  currentPage,
  totalPages,
  pageSize,
  totalItems,
  onPageChange,
  onPageSizeChange
}: PaginationWithSizeProps) {
  const startItem = (currentPage - 1) * pageSize + 1
  const endItem = Math.min(currentPage * pageSize, totalItems)
 
  return (
    <div className="pagination-container">
      {/* Page info */}
      <p className="pagination__info" aria-live="polite">
        Showing {startItem} to {endItem} of {totalItems} results
      </p>
 
      {/* Page size selector */}
      <div className="pagination__size">
        <label htmlFor="page-size">Items per page:</label>
        <select
          id="page-size"
          value={pageSize}
          onChange={(e) => onPageSizeChange(Number(e.target.value))}
        >
          <option value={10}>10</option>
          <option value={25}>25</option>
          <option value={50}>50</option>
          <option value={100}>100</option>
        </select>
      </div>
 
      {/* Pagination controls */}
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={onPageChange}
      />
    </div>
  )
}

URL-Based Pagination (Next.js)

'use client'
 
import { useRouter, useSearchParams } from 'next/navigation'
 
interface URLPaginationProps {
  totalPages: number
}
 
export function URLPagination({ totalPages }: URLPaginationProps) {
  const router = useRouter()
  const searchParams = useSearchParams()
  const currentPage = Number(searchParams.get('page')) || 1
 
  const handlePageChange = (page: number) => {
    const params = new URLSearchParams(searchParams)
    params.set('page', String(page))
    router.push(`?${params.toString()}`)
  }
 
  return (
    <Pagination
      currentPage={currentPage}
      totalPages={totalPages}
      onPageChange={handlePageChange}
    />
  )
}

Styling

.pagination__list {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
 
.pagination__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.5rem;
  height: 2.5rem;
  padding: 0 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: #fff;
  color: #333;
  font-size: 0.875rem;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}
 
.pagination__button:hover:not(:disabled) {
  background: #f5f5f5;
  border-color: #999;
}
 
.pagination__button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}
 
.pagination__button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
 
.pagination__button--current {
  background: #0066cc;
  border-color: #0066cc;
  color: #fff;
  font-weight: 600;
}
 
.pagination__button--current:hover {
  background: #0052a3;
}
 
.pagination__ellipsis {
  padding: 0 0.5rem;
  color: #999;
}
 
.pagination__info {
  font-size: 0.875rem;
  color: #666;
}
 
/* Responsive */
@media (max-width: 640px) {
  .pagination__list {
    flex-wrap: wrap;
    justify-content: center;
  }
 
  /* Hide page numbers, show only prev/next */
  .pagination__list li:not(:first-child):not(:last-child) {
    display: none;
  }
}
 
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

Verification

  1. Navigate with keyboard (Tab through all controls)
  2. Verify current page is announced by screen reader
  3. Check aria-labels are descriptive
  4. Test disabled state on first/last page
  5. Verify page change announcements
  6. Check focus returns appropriately after page change
  7. Test with screen reader navigation
Announce Page Changes

When pagination changes content without a full page reload, use aria-live regions to announce the change. Users should know content has updated.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Verify pagination uses nav element with aria-label, indicates current page with aria-current, and is keyboard navigable.

Fix

Auto-fix issues

Implement pagination with nav role='navigation', aria-label, aria-current='page' for active page, and focusable controls.

Explain

Learn more

Explain how accessible pagination helps screen reader users understand their position and navigate through paged content.

Review

Code review

Review templates, server-rendered HTML, and shared components that output markup related to Make pagination accessible. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Nu Html Checker
validator.w3.orgTool
Pagination Pattern – UX Patterns for Developers

Comprehensive UX pattern guide covering anatomy, accessibility, best practices, and implementation.

uxpatterns.devGuide

Rules that often go hand-in-hand with this one.

Implement accessible breadcrumb navigation

Breadcrumb navigation is implemented with proper semantic markup and ARIA attributes for accessibility.

HTML
Make carousels accessible

Carousels and sliders are accessible with pause controls, keyboard navigation, and proper ARIA attributes.

Accessibility
Make notifications accessible

Toast notifications and alerts are announced to screen readers using ARIA live regions and appropriate roles.

Accessibility
Make drag and drop accessible

Drag and drop interfaces provide keyboard alternatives and proper ARIA attributes for accessibility.

Accessibility

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385