Truly Responsive Pagination

14 June 2020

Searching the web for an example of a fully responsive pagination widget I found that either people consider “responsive” to be something else or that it is a non-trivial problem to solve. In any case, I decided to have a crack at it myself.

A sketch of how pagination widget should work

My definition of a responsive pagination widget is pretty simple: such a widget should maintain its look and feel when the viewport (or a window) changes the size, yet the excessive information such as page numbers in the middle should be omitted if there is no space for them.

In this post I am going to describe my journey on the topic and the solution I ended up with.

My starting point was pretty simple – I had the following HTML markup structure that was representing the page numbers in a pagination widget:

<span class="paginator">
<ul>
<li><a href="#">&lt;</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">6</a></li>
<li><a href="#">7</a></li>
<li><a href="#">8</a></li>
<li><a href="#">9</a></li>
<li><a href="#">10</a></li>
<li><a href="#">&gt;</a></li>
</ul>
</span>

The goal was to create a fully responsive widget using pure CSS and HTML and to try to avoid messing up the HTML structure unnecessarily, since I needed to preserve the “cleanness” of navigation for screen readers (i.e. accessibility is important!).

After a bit of brainstorming I ended up with two candidates for the implementation of my idea:

  1. the “old-school” approach of presenting a list as a line of inline-block elements floating next to each other;
  2. the recently new trend in web design to use the flexbox model.

Since I did not use display: flex before, I decided to try the flexbox layout first. The idea was that I could use two flex rows which wrap when the width was shrinking. The challenge was that I wanted to implement two visual effects:

  1. the currently selected page should be in the middle of the pagination widget and the wrapping of the rows should be adjacent to that selected page number;
  2. it should be visually unambiguous that the ranges of off-screen pages were omitted.

After thinking a bit I realised that the above description demands to have three logically separate components: a part before the selected page number (I call it “head”), a part containing the selected page number (I call it “middle”), and a part following the selected page number (I call it “tail”). Hence, my HTML markup became:

<span class="paginator">
<ul class="head">
<li><a href="#">&lt;</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
</ul>
<ul class="middle">
<li><a href="#">5</a></li>
</ul>
<ul class="tail">
<li><a href="#">6</a></li>
<li><a href="#">7</a></li>
<li><a href="#">8</a></li>
<li><a href="#">9</a></li>
<li><a href="#">10</a></li>
<li><a href="#">&gt;</a></li>
</ul>
</span>

Initially, I just laid out the lists in a line just to see how it looks and whether I like the mechanics of it, so I started with the following style definitions:

.paginator {
/* width: 50%; */
/* margin: 0 auto; */
display: flex;
white-space: nowrap;
overflow: hidden;
min-width: 15em; /* 5 items 3em each */
}
ul {
display: flex;
position: relative;
flex-flow: row wrap;
list-style: none;
margin: 0;
padding: 0;
line-height: 1.2;
height: 3.2em;
overflow: hidden;
min-width: 6em;
}
ul li {
display: block;
width: 3em;
height: 3em;
line-height: 3;
}
ul li a {
display: block;
text-align: center;
text-decoration: none;
}
ul.middle {
min-width: 3em;
}

The above produced the following result (the example is wrapped in a resizable <div>, so if you are using a browser that supports it, like Chrome, you should be able to play with it right away):

There are two issues which are immediately manifesting themselves when you resize the widget:

  1. The “tail” section wraps at the end truncating the list of the available pages and making it impossible to jump to the last page number in the list;
  2. There is no visual guidance whatsoever that the page number were omitted.

The first issue is a difficult one since in the flexbox model we cannot easily control which side of the row is used for wrapping. However, if we adjust our HTML markup and leverage not so often used row-reverse value of the flexbox, then achieve the desired effect.

So, in the HTML markup I reversed the “tail” list:

<ul class="tail">
<li><a href="#">&gt;</a></li>
<li><a href="#">10</a></li>
<li><a href="#">9</a></li>
<li><a href="#">8</a></li>
<li><a href="#">7</a></li>
<li><a href="#">6</a></li>
</ul>

and I adjusted the style of the “tail” list to apply row-reverse (which basically visually presents the list in the reverse order):

ul.tail {
flex-flow: row-reverse wrap;
}

With the above changes applied the result looks much more closer to what we want:

Now, to tackle with the visual guidance for the omitted page numbers we can do something like the following: we can display dots in space when the page number is being wrapped. To do so we could use pseudo-elements for the “middle” list:

ul.middle::before {
content: '';
width: 300%;
height: 1em;
position: absolute;
z-index: -1;
border-top: dotted;
left: -100%;
top: 1.5em;
}

A few things to note: we are creating a pseudo-element which is three times wider than its parent, we position it absolutely, shift it 1/3 to the left and half way down. With the only border defined to be the top border we are getting a dotted line that goes through the middle of our “middle” list.

However, to make this work we also need to ensure that the border we created can extend beyond the box defined by the “middle” list, hence we need to set overflow to visible and let the border overflow the boundaries:

ul.middle {
overflow: visible; /* show the background dot line */
}

Finally, some additional cosmetic touches are required:

  • the list items have transparent background and the border line is visible behind the page numbers, so to address this we need to explicitly set the background property for list items;
  • to overlay our explicitly defined background on top of the border line we need to bump our list items up in the rendering order by setting z-index to be higher;
  • we need to handle a situation when the widget is compressed to its minimal size so there are just three page numbers: the first, the current, and the last page number – in this situation we would not have any space between list items, yet we need to indicate that the page ranges were omitted – we can do this by playing with min-width for the “head” and “tail” lists.

The above translates to the following styling rules:

ul li {
z-index: 1;
background: whitesmoke;
}
ul.head,
ul.tail {
min-width: 6.5em;
}

OK, it is time to see the result of all these changes:

Although the result looks promising and is a workable solution, there is still a couple of issues left unsolved:

  1. The biggest concern I have with this solution is that it requires reordering of the “tail” list and it is messing up the screen readers, therefore making it much harder for people with assistive technologies to use it;
  2. I also discovered an edge case if the current page was adjacent to “head” (e.g. the second page) or to “tail” (e.g. the second to last page) the range dots would be still present in that case since they are regulated by that .5em space I left for the “head” and “tail” lists in the min-width attribute.

The first issue may be solved by introducing a shadow copy of the pagination widget that is designed specifically for the screen readers while making this visually pleasing widget hidden from the screen readers.

The second issue is much more puzzling and so far I could not find a pure HTML + CSS solution for it. It can be addressed by a bit of JavaScript, but I did not want to go that route for my blog. Most likely, since my blog is statically generated I will address it using the logic of the generator itself.

Still, I think the techniques I demonstrated here may be quite useful to some people who are designing pure HTML and CSS adaptive widgets.