The best damn pagination component

So you’ve got a whole bunch of content. Too much to present on one page. It’s time to start thinking about pagination. Hell, maybe it was time to start thinking about pagination earlier than this, but you’re here now.

Gav McKenzie
Author
Gav McKenzie
Published
Mar 26, 2020
Topics
How-to, Industry, Engineering

We're going to look into when you should use pagination vs infinite scrolling, what makes a good pagination component, how to make your pagination accessible and finally... the ultimate pagination component.

TL;DR Give me the code

/apps/blog/posts/the-best-damn-pagination-component/Screenshot_2020-05-21_at_14.35.19.png

You can install our pagination component for Next.js from NPM or just fork/borrow the code on Github. Make sure to have a look at the demo to see if you like it first.

Pagination vs infinite scroll

With everyone staring at their phones all day scrolling through social media, a lot of people will ask you if they should be using infinite scrolling instead of pagination. As always, the controls you use depend on the content you are presenting.

Infinite scroll works great for something like social media where users are consuming an endless stream of throwaway content without looking for something specific.

Pagination really shines on search results when people are looking for a specific item and want to be able to consistently find that item again (or link to the results).

Pagination essentials

Still here? It sounds like you need some pagination! So what goes in to a good pagination component? Tons of sites have them and tons of them do it badly.

Let’s go through some rules for good pagination.

Show the current page

Before you can find out where you're going, you need to know where you are. Clearly identify which page users are on and don’t let them try to re-navigate to this page. Nothing more frustrating than having to watch the page you're already on reload for no reason.

Next and previous links

The next step is to add a link to the next page and the previous page. If the current page is the first or last, then the next or previous links should be disabled, respectively. If possible, don’t let users navigate to empty pages of results.

Individual page links

The next step after next and previous links is to have links to specific pages, such as 1/2/3. This lets your users skip a page or two if they think they need to. Don’t show too many though. If you have lots of results, it’s best highlight a few individual page links near the current page.

First and last links

Caveat: this is not always appropriate. You'll have to make a judgement call. First and last links are usually used in publications such as blogs or web comics. If you do choose to use them, first and last links should be placed on the outside of the pagination, around the previous and next links. You could also choose to always show the numerical link to the first and last pages, such as 1 and 500.

Page selection input

For really long numbers of pages, you may want to include a page selection input. This allows users to enter the page number they want and navigate directly to the page. For smaller numbers of pages where you can show all the options, it can be rather superfluous.

Page length

It’s often useful to provide an input for selecting the length of each page. Users can decide themselves whether they would prefer, shorter, simpler pages or less http requests. Rather than using a text input here, a select is usually preferable to discourage strange page lengths.

Large click areas

This is really a fundamental of interaction UI, but I'm going to remind you anyway. Click/touch targets should be fat finger friendly. Apple recommend a minimum of 44x44px with Microsoft going with a more lenient 40x40px. Touch targets should be well spaced to avoid accidentally clicking the neighbouring target.

GET variables

In order to not affect the current url structure, but still provide a returnable, or shareable, link the pagination should set get variables such as page and limit.

No JavaScript

No JavaScript in 2020!? Are you mad?

Sometimes things go wrong and you should always make sure your site works, even if the JavaScript doesn't. Maybe it fails to download, maybe there’s a strange error that kills the page. You never know, so provide fallbacks for when the JS fails.

Don’t pass unused variables

If you are using the default page size limit, there’s no need to pass it every time. ?page=1 is always neater than ?page=1&size=20

Return to page 1 when the page size changes

You can get into a real pickle and easily some 404s if you don’t revert to page 1 when the page size changes. It’s not the most elegant, but it’s much simpler for you and the user to know where you are again.

Accessibility

The web is for everyone. Making an accessible component is usually the first step to making a good component.

  • Host the component in a <nav> element or add role="navigation".
  • Add aria-label="pagination" to the wrapper to describe the type of navigation.
  • Add aria-label="Page X" to each link so it’s not just a number.
  • Add aria-label="Page X, current page" to the label that points to the current page.
  • Add aria-current="page" to the label that points to the current page.
  • Add aria-disabled="true" to the pagination links when disabled.

Putting it together

OK let’s take the pagination essentials and our accessibility requirements and mash them together into a glorious component.

The interface

Here’s an example of the markup for the final pagination component. Note that this basic example doesn’t have first/last links or an input to specifically set the page number.

The example is set up with the current page as 1 and the selected page size as 40.

<nav aria-label="pagination" class="pagination">
<ul class="pagination__list">
<li class="pagination__item">
<span class="pagination__prev pagination__prev--disabled" aria-label="Previous page">Previous</span>
</li>
<li class="pagination__item">
<strong class="pagination__link pagination__link--current" aria-label="Page 1, current page" aria-current="page">1</strong>
</li>
<li class="pagination__item">
<a class="pagination__link" href="?page=2&size=40" aria-label="Page 2">2</a>
</li>
<li class="pagination__item">
<a class="pagination__link" href="?page=3&size=40" aria-label="Page 3">3</a>
</li>
<li class="pagination__item">
<a class="pagination__link" href="?page=4&size=40" aria-label="Page 4">4</a>
</li>
<li class="pagination__item">
<a class="pagination__link" href="?page=5&size=40" aria-label="Page 5">5</a>
</li>
<li class="pagination__item">
<a class="pagination__next" href="?page=3&size=40" aria-label="Next page">Next</a>
</li>
</ul>
<div class="pagination__size">
<form method="GET" action="" class="pagination__form">
<input type="hidden" name="page" value="1" />
<label for="size">Per page</label>
<select name="size" id="size">
<option>20</option>
<option selected>40</option>
<option>60</option>
<option>80</option>
<option>100</option>
</select>
<button type="submit">Go</button>
</form>
</div>
</nav>

We then add some basic CSS to tidy up the layout, set the touch target sizes and hide the individual page numbers on mobile browsers to prevent the layout breaking on small screens.

.pagination {
display: flex;
justify-content: space-between;
}
.pagination__list {
display: flex;
list-style-type: none;
margin: 0;
padding: 0;
}
.pagination__link,
.pagination__first,
.pagination__last,
.pagination__prev,
.pagination__next,
.pagination__size {
display: block;
line-height: 1.5;
padding: 0.5em 1em;
}
.pagination__item {
display: none;
}
.pagination__item:first-child,
.pagination__item:last-child {
display: block;
}
@media screen and (min-width: 40em) {
.pagination__item {
display: block;
}
}

You can try this out in our live example.

The logic

The logic is a little more complicated, so rather than showing you the code, I'm going to suggest a list of things to consider.

  • Show up to 6 individual page links.
  • Always show the first/last page links.
  • Show up to 4 other page links. These should be close to the current page.
  • Show gaps between individual page links. E.g. < 1 ... 5 6 7 ... 500 >
  • Disable links with no results, such as “previous” on the first page.

The component

If this all sounds like too much (and you happen to use Next.js), we've got you covered.

/apps/blog/posts/the-best-damn-pagination-component/Screenshot_2020-05-21_at_14.47.32.png

The next pagination component is available on NPM and comes packed with all the considerations we've mentioned so far.

  • Accessible. Semantic HTML and fully marked up with appropriate aria roles for assisted browsing.
  • Usable. The UI styles account for keyboard focus states and fat finger touch targets.
  • Responsive. Works on all devices.
  • Themeable. Make it look however you want.
  • Self contained. There’s only one required prop to get going. The rest of the logic is handled for you.
  • Works with Next. Integrated with the Next.js router.

Dealing with edge cases

As the pagination component uses GET variables to set the url, it’s possible you could get some strange inputs from users manually updating the url.

Make sure your site can handle these strange inputs and either redirect the user back to somewhere safe (such as page 1) or throw a 404.

  • page=-1
  • page=999999 (Something higher than your total number of results)
  • limit=-1
  • limit=999999 (Something higher than your limit allows)
  • SQL injection. Always sanitise your inputs!

Other libraries

There’s a bunch of other pagination components out there that you might like to use instead. Here’s a selection and why we chose not to use them.

Further reading