Skip to main content

Overview

The Pagination component provides page navigation controls for multi-page content. It includes previous/next buttons, individual page number links, and automatic URL parameter management.

Usage

import { Pagination } from '@/components'
import { useState } from 'react'

function JobSearchPage() {
  const [currentPage, setCurrentPage] = useState(1)
  
  const handlePageChange = (newPage) => {
    setCurrentPage(newPage)
    // Fetch jobs for the new page
  }
  
  return (
    <Pagination
      currentPage={currentPage}
      totalPages={10}
      onPageChange={handlePageChange}
    />
  )
}

Props

currentPage
number
default:1
The currently active page number (1-based index)
totalPages
number
default:10
The total number of pages available
onPageChange
function
required
Callback function called when the user changes pages. Receives the new page number as an argument.
onPageChange: (page: number) => void

Features

Previous/Next Navigation

The component includes arrow buttons for navigating to the previous or next page:
const isFirstPage = currentPage === 1
const isLastPage = currentPage === totalPages

const stylePrevButton = isFirstPage ? { pointerEvents: 'none', opacity: 0.5 } : {}
const styleNextButton = isLastPage ? { pointerEvents: 'none', opacity: 0.5 } : {}
Buttons are automatically disabled when at the first or last page.

Direct Page Navigation

Users can click on any page number to jump directly to that page:
{pages.map((page) => (
  <a
    key={page}
    data-page={page}
    href="#"
    className={currentPage === page ? styles.isActive : ''}
    onClick={handleChangePage}
  >
    {page}
  </a>
))}

URL Parameter Management

The component updates the URL with the current page:
const buildPageUrl = (page) => {
  const url = new URL(window.location)
  url.searchParams.set('page', page)
  return `${url.pathname}?${url.searchParams.toString()}`
}
This allows users to bookmark specific pages and use browser back/forward buttons.

Component Structure

import styles from './Pagination.module.css'

export function Pagination ({ currentPage = 1, totalPages = 10, onPageChange }) {
  const pages = Array.from({ length: totalPages }, (_, i) => i + 1)

  const isFirstPage = currentPage === 1
  const isLastPage = currentPage === totalPages

  const stylePrevButton = isFirstPage ? { pointerEvents: 'none', opacity: 0.5 } : {}
  const styleNextButton = isLastPage ? { pointerEvents: 'none', opacity: 0.5 } : {}

  const handlePrevClick = (event) => {
    event.preventDefault()
    if (isFirstPage === false) {
      onPageChange(currentPage - 1)
    }
  }

  const handleNextClick = (event) => {
    event.preventDefault()
    if (isLastPage === false) {
      onPageChange(currentPage + 1)
    }
  }

  const handleChangePage = (event) => {
    event.preventDefault()
    const page = Number(event.target.dataset.page)

    if (page !== currentPage) {
      onPageChange(page)
    }
  }

  const buildPageUrl = (page) => {
    const url = new URL(window.location)
    url.searchParams.set('page', page)
    return `${url.pathname}?${url.searchParams.toString()}`
  }

  return (
    <nav className={styles.pagination}>
      <a href={buildPageUrl(currentPage-1)} style={stylePrevButton} onClick={handlePrevClick}>
        <svg>{/* Previous arrow icon */}</svg>
      </a>

      {pages.map((page) => (
        <a
          key={page}
          data-page={page}
          href="#"
          className={currentPage === page ? styles.isActive : ''}
          onClick={handleChangePage}
        >
          {page}
        </a>
      ))}

      <a href={buildPageUrl(currentPage+1)} style={styleNextButton} onClick={handleNextClick}>
        <svg>{/* Next arrow icon */}</svg>
      </a>
    </nav>
  )
}

Styling

The component uses CSS modules (Pagination.module.css) for styling:
  • styles.pagination - Main navigation container
  • styles.isActive - Applied to the current page number

Disabled State Styling

Disabled buttons use inline styles:
{ pointerEvents: 'none', opacity: 0.5 }

Event Handling

Page Change Handler

The onPageChange callback is called with the new page number:
const handlePageChange = (newPage) => {
  console.log('Navigating to page:', newPage)
  // Update state
  setCurrentPage(newPage)
  // Fetch new data
  fetchJobs(newPage)
  // Scroll to top
  window.scrollTo({ top: 0, behavior: 'smooth' })
}

Preventing Default

All click handlers prevent the default link behavior:
const handlePrevClick = (event) => {
  event.preventDefault()
  // ...
}

Usage Patterns

With URL Synchronization

import { Pagination } from '@/components'
import { useSearchParams } from 'react-router-dom'

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams()
  const currentPage = Number(searchParams.get('page')) || 1
  
  const handlePageChange = (page) => {
    setSearchParams({ page: page.toString() })
  }
  
  return (
    <Pagination
      currentPage={currentPage}
      totalPages={10}
      onPageChange={handlePageChange}
    />
  )
}

With API Pagination

import { Pagination } from '@/components'
import { useState, useEffect } from 'react'

function ApiPaginatedList() {
  const [page, setPage] = useState(1)
  const [totalPages, setTotalPages] = useState(0)
  const [items, setItems] = useState([])
  
  useEffect(() => {
    fetch(`/api/items?page=${page}`)
      .then(res => res.json())
      .then(data => {
        setItems(data.items)
        setTotalPages(data.totalPages)
      })
  }, [page])
  
  return (
    <>
      <ItemList items={items} />
      <Pagination
        currentPage={page}
        totalPages={totalPages}
        onPageChange={setPage}
      />
    </>
  )
}

With Scroll to Top

const handlePageChange = (newPage) => {
  setCurrentPage(newPage)
  
  // Smooth scroll to top
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
  
  // Or scroll to specific element
  document.querySelector('.jobs-listings')?.scrollIntoView({
    behavior: 'smooth'
  })
}

Accessibility

  • Uses semantic <nav> element for the pagination controls
  • Links are keyboard accessible
  • Disabled states use pointer-events: none and reduced opacity
  • Active page is visually indicated with the isActive class

Improvements

For better accessibility, consider:
<nav className={styles.pagination} aria-label="Pagination">
  <a 
    href={buildPageUrl(currentPage-1)} 
    style={stylePrevButton} 
    onClick={handlePrevClick}
    aria-label="Previous page"
    aria-disabled={isFirstPage}
  >
    {/* ... */}
  </a>
  
  {pages.map((page) => (
    <a
      key={page}
      aria-label={`Page ${page}`}
      aria-current={currentPage === page ? 'page' : undefined}
    >
      {page}
    </a>
  ))}
</nav>

Performance Considerations

Large Page Counts

For pagination with many pages (e.g., 100+), consider implementing ellipsis:
function getVisiblePages(current, total) {
  if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
  
  if (current <= 4) return [1, 2, 3, 4, 5, '...', total]
  if (current >= total - 3) return [1, '...', total - 4, total - 3, total - 2, total - 1, total]
  
  return [1, '...', current - 1, current, current + 1, '...', total]
}

Source Code

Location: src/components/Pagination/Pagination.jsx:3

JobListings

Render paginated job listings

Components Overview

View all available components

Best Practices

  1. Always provide onPageChange - This callback is required for the component to function
  2. Sync with URL - Update URL parameters when the page changes for shareable links
  3. Scroll to top - Automatically scroll to the top when changing pages
  4. Loading states - Show a loading indicator while fetching new page data
  5. Validate page numbers - Ensure currentPage and totalPages are valid numbers
  6. Consider page size - For large datasets, implement ellipsis to avoid rendering too many page links

Build docs developers (and LLMs) love