Accessible components: Pagination

Mica - Oct 29 - - Dev Community

Today, we're going to look at how to create pagination from scratch and make it accessible and reusable. I hope it’s helpful, and please leave your comments at the end of the post!

Github: https://github.com/micaavigliano/accessible-pagination
Proyecto: https://accessible-pagination.vercel.app/

Custom hook to fetch data

const useFetch = <T,>(url: string, currentPage: number = 0, pageSize: number = 20) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<boolean>(false);

  useEffect(() => {
    const fetchData = async() => {
      setLoading(true);
      setError(false);

      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('network response failed')
        }
        const result: T = await response.json() as T;
        setData(result)
      } catch (error) {
        setError(true)
      } finally {
        setLoading(false);
      }
    };

    fetchData()
  }, [url, currentPage, pageSize]);

  return {
    data,
    loading,
    error,
  }
};
Enter fullscreen mode Exit fullscreen mode
  1. We'll create a custom hook with a generic type. This will allow us to specify the expected data type when using this hook.
  2. We'll expect three parameters: one for the URL from which we'll fetch the data, currentPage, which is the page we're on (default is 0), and pageSize, which is the number of items per page (default is 20, but you can change this value).
  3. In our state const [data, setData] = useState<T | null>(null);, we use the generic type T since, as we use it for different data requests, we'll be expecting different data types.

Pagination

To make pagination accessible, we need to consider the following points:

  • Focus should move through all interactive elements of the pagination and have a visible indicator.
  • To ensure good interaction with screen readers, we must correctly use regions, properties, and states.
  • Pagination should be grouped within a <nav> tag and contain an aria-label identifying it specifically as pagination.
  • Each item within the pagination should have an aria-setsize and an aria-posinset. Now, what are these for? Well, aria-setsize is used to calculate the total number of items within the pagination list. The screen reader will announce it as follows:

Screenshot of a voiceover annoucement: list of 1859 items

aria-posinset is used to calculate the position of the item within the total number of items in the pagination. The screen reader will announce it as follows:

Screenshot of the screen reader voiceover that announces: go to page 1. Current page, button, position 1 of 1859

  • Each item should have an aria-label to indicate which page we’ll go to if we click on that button.
  • Include buttons to go to the next/previous item, and each of these buttons should have its corresponding aria-label.
  • If our pagination contains an ellipsis, it should be correctly marked with an aria-label.
  • Every time we go to a new page, the screen reader should announce which page we are on and how many new items there are, as follows:

Screenshot of the screen reader voiceover that announces: page 3 loaded. Showing 20 items

To achieve this, we’re going to code it as follows:

const [statusMessage, setStatusMessage] = useState<string>("");

useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
    if (!loading) {
      setStatusMessage(`Page ${currentPage} loaded. Displaying ${data?.near_earth_objects.length || 0} items.`);
    }
  }, [currentPage, loading]);
Enter fullscreen mode Exit fullscreen mode

When the page finishes loading, we’ll set a new message with our currentPage and the length of the new array we’re loading.

Alright! Now let’s take a look at how the code is structured in the pagination.tsx file.

The component will require five props.

interface PaginationProps {
  currentPage: number;
  totalPages: number;
  nextPage: () => void;
  prevPage: () => void;
  goToPage: (page: number) => void;
}
Enter fullscreen mode Exit fullscreen mode
  • currentPage refers to the current page. We’ll manage it within the component where we want to use pagination as follows: const [currentPage, setCurrentPage] = useState<number>(1);
  • totalPages refers to the total number of items to display that the API contains.
  • nextPage is a function that will allow us to go to the next page and update our currentPage state as follows:
const handlePageChange = (newPage: number) => {
    setCurrentPage(newPage); 
  };

  const nextPage = () => {
    if (currentPage < totalPages) {
      handlePageChange(currentPage + 1);
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • prevPage is a function that will allow us to go to the page before our current page and update our currentPage state.
const prevPage = () => {
    if (currentPage > 1) {
      handlePageChange(currentPage - 1);
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • goToPage is a function that will need a numeric parameter and is the function each item will use to navigate to the desired page. We’ll make it work as follows:
const handlePageChange = (newPage: number) => {
    setCurrentPage(newPage); 
};
Enter fullscreen mode Exit fullscreen mode

To bring our pagination to life, we need one more step: creating the array that we’ll iterate through in our list! For that, we should follow these steps:

  1. Create a function; I’ll call it getPageNumbers in this case.
  2. Create variables for the first and last items in the list.
  3. Create a variable for the left-side ellipsis. I’ve decided to place my ellipsis after the fourth item in the list.
  4. Create a variable for the right-side ellipsis. I’ve decided to place my ellipsis just before the last three items in the list.
  5. Create a function that returns an array with 5 centered items: the current page, two previous items, and two subsequent items. If needed, we’ll exclude the first and last pages. const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
  6. For our final variable, we’ll create an array that contains all the previously created variables.
  7. Finally, we’ll filter out null elements and return the array. This array is what we’ll iterate through to get the list of items in our pagination as follows:
<ol className='flex gap-3'>
          {pageNumbers.map((number) => {
            if (number === 'left-ellipsis' || number === 'right-ellipsis') {
              return (
                <span key={number} className='relative top-5' aria-label='ellipsis'>
                  ...
                </span>
              );
            }
            return (
              <li aria-setsize={totalPages} aria-posinset={typeof number === 'number' ? number : undefined} key={`page-${number}`}>
                <button
                  onClick={() => goToPage(Number(number))}
                  className={currentPage === Number(number) ? 'underline underline-offset-3 border-zinc-300' : ''}
                  aria-label={`go to page ${number}`}
                  aria-current={currentPage === Number(number) && 'page'}
                >
                  {number}
                </button>
              </li>
            );
          })}
        </ol>
Enter fullscreen mode Exit fullscreen mode

And that’s how to create a reusable and accessible pagination! Personally, I learned how to build pagination from scratch the hard way because I had to implement it in a live coding session. I hope my experience helps you in your career and that you can implement it and even improve it!

You can follow me:
linkedin: https://www.linkedin.com/in/micaelaavigliano/

Best regards,
Mica <3

. . . . . . . . . .