Building a paginated listing with only HTML and CSS for Responsive Design

Est. Reading Time: 17 minutes

Most paginated listings on the web are managed either with server side logic, javascript, or a combination of both. With the first option, the user’s browser wait on network traffic back to the server and back down again. Even if the data is as succinct as possible, a bad connection or crowded wi-fi can make your site look slow. An example of this sort of method can be seen on google search results. Each page requires a new querystring and page load. With the second option, the html for each page can be loaded into the dom on the first page load and then a script will manage displaying each separate page or section of data as the user interacts with the page’s navigation. The downside of this approach is that if script is disabled, as is so often the case on computers with high security settings, the only ways to recover are to display either a single page at a time using server side logic, or to display the entire listing without pagination at all, which can make for an enormously long page. The last option uses a combination of both, and can result in a smoother user experience, but also then suffers from the drawbacks of both. This last option can be seen in effect by scrolling to the bottom of a facebook page; the first “page” of content is loaded up first, and only by scrolling down is additional content then called down from the server and added to the page via javascript.

I think that’s enough of the bad news! You’re here to find a way to circumvent all of these headaches so that you can give even the most restrictive browsers as much access to your content and with the very best user experience possible.

Lets get started shall we?

We’ll start with a sample of how you might list results currently. For ease of reading and instruction we’ll keep the content short and sweet, and we’ll keep our pages limited to 3 results per page.

<div class="pages">
    <div class="title"><a href="/article1.htm">Page 1</a></div>
    <div class="title"><a href="/article2.htm">Page 2</a></div>
    <div class="title"><a href="/article3.htm">Page 3</a></div>
    <div class="title"><a href="/article4.htm">Page 4</a></div>
    <div class="title"><a href="/article5.htm">Page 5</a></div>
    <div class="title"><a href="/article6.htm">Page 6</a></div>
    <div class="title"><a href="/article7.htm">Page 7</a></div>
    <div class="title"><a href="/article8.htm">Page 8</a></div>
    <div class="title"><a href="/article9.htm">Page 9</a></div>
</div>

We have to make a few changes to this to make it work in pages. Let see what that looks like:


<div class="cssPagination">
    <div class="pages">
        <div class="page" id="page2">
            <div class="title">
                <a href="/article4.htm">Page 4</a>
            </div>
            <div class="title">
                <a href="/article5.htm">Page 5</a>
            </div>
            <div class="title">
                <a href="/article6.htm">Page 6</a>
            </div>
        </div>
        <div class="page" id="page3">
            <div class="title">
                <a href="/article7.htm">Page 7</a>
            </div>
            <div class="title">
                <a href="/article8.htm">Page 8</a>
            </div>
            <div class="title">
                <a href="/article9.htm">Page 9</a>
            </div>
        </div>
        <div class="page" id="page1">
            <div class="title">
                <a href="/article1.htm">Page 1</a>
            </div>
            <div class="title">
                <a href="/article2.htm">Page 2</a>
            </div>
            <div class="title">
                <a href="/article3.htm">Page 3</a>
            </div>
        </div>
    </div>
</div>


Now that we’ve got them separated into page containers there’s one thing that’s important to make note of. You may have noticed that page1 is actually listed last. It’s also important to note that the order of all pages that aren’t the first page isn’t relevant to this process, only that the page you want displayed by default, or first, is the last child of the .pages container. This is so that we can take advantage of a special kind of css selector in order to get around css’s inability to select nodes up the dom. If you intend to use my advanced method below however, it is necessary to display the pages in order with the only exception still being that the first page be listed last.

That selector is called the General Sibling selector, which is the last selector outlined here. This selector selects all elements that match the right hand selector that are siblings of the left hand selector only as long as they appear in the html after the left hand selector. In our sample, a rule for “#page3 ~ .page” would select #page2, and #page1, but “#page2 ~ .page” would only select #page1.

We’re going to use that selector to hide all pages that aren’t the first one.

<style type="text/css">
.cssPagination .pages > .page {display: none;}
.cssPagination .pages > :last-child {display: block;}
</style>

This ensures that when the webpage loads, only one page of your list will display, and that it will be the first page. In order to give the user a way to change the pages, we need to create a page listing in our html.

<div class="cssPagination">
    <div class="pages">
        <div class="page" id="page2">
            <div class="title">
                <a href="/article4.htm">Page 4</a>
            </div>
            <div class="title">
                <a href="/article5.htm">Page 5</a>
            </div>
            <div class="title">
                <a href="/article6.htm">Page 6</a>
            </div>
        </div>
        <div class="page" id="page3">
            <div class="title">
                <a href="/article7.htm">Page 7</a>
            </div>
            <div class="title">
                <a href="/article8.htm">Page 8</a>
            </div>
            <div class="title">
                <a href="/article9.htm">Page 9</a>
            </div>
        </div>
        <div class="page" id="page1">
            <div class="title">
                <a href="/article1.htm">Page 1</a>
            </div>
            <div class="title">
                <a href="/article2.htm">Page 2</a>
            </div>
            <div class="title">
                <a href="/article3.htm">Page 3</a>
            </div>
        </div>
    </div>
    <div class="pageNav">
        <a class="pageNumber" href="#page1">1</a>
        <a class="pageNumber" href="#page2">2</a>
        <a class="pageNumber" href="#page3">3</a>
    </div>
</div>

The new .pageNav element can go anywhere on the page, but it must not be added as the last child of the .pages div so that our previous rules will continue to show and hide the correct divs. To get those links to functionally show their respective pages we need to make an additional tweak to our css:

<style type="text/css">
.cssPagination .pages > .page:target ~ .page:last-child,
.cssPagination .pages > .page {display: none;}
.cssPagination .pages > :last-child,
.cssPagination .pages > .page:target {display: block;}
</style>

These new rules showcase the two selectors that make this whole thing work. One we’ve already discussed. The new one is the :target selector, which applies to an element if it has an id that matches the hashtag (for lack of a better word) in the url. When one of the nav links is clicked, it appends it’s href to the url. A live demo of this in action is available here.

The key to this html/css trick is just outputting your first page last, which is easily possible in most content management systems. The rest of it is very simple. This solution is responsive, is 508 compliant, and search engine friendly.

Another benefit this method has over javascript is that it take advantage of the browser’s ability to remember the previous page’s hash tag, which means that if a user follows a link in your paginated listing, and clicks the back button, they’ll retain their active page.

To be fair to the other pagination methods above, this too comes with a drawback, although it is in my opinion a minor drawback. In order for this method to be fully encapsulated by html and css all html content for all pages must be loaded on the first page load, which can slow down the page if the content of each page is exceptionally large. In the case of most paginated listings however, that is rarely the case.

Advanced

Now that the base concepts have been outlined, I’d like to show you what I consider to be the correct and most exhaustive solution for applying this sort of method to a responsive site where we don’t want to limit the user to only the core html/css experience if they’ve allowed scripts on their browser.

NOTE: This implementation uses jQuery for ease of understanding, and portability. jQuery is, however, a very large library and might not be the most efficient way of accomplishing this from a performance perspective.

First, lets look at some easy changes to the html:

<div class="cssPagination">
    <div class="pages">
        <div class="page" id="page2">
            <div class="title">
                <a href="/article4.htm">Page 4</a>
            </div>
            <div class="title">
                <a href="/article5.htm">Page 5</a>
            </div>
            <div class="title">
                <a href="/article6.htm">Page 6</a>
            </div>
            <a class="prev page2" href="#page1">prev</a>
            <a class="next page2" href="#page3">next</a>
        </div>
        <div class="page" id="page3">
            <div class="title">
                <a href="/article7.htm">Page 7</a>
            </div>
            <div class="title">
                <a href="/article8.htm">Page 8</a>
            </div>
            <div class="title">
                <a href="/article9.htm">Page 9</a>
            </div>
            <a class="prev page3" href="#page2">prev</a>
            <a class="next page3 disabled" href="#page3">next</a>
        </div>
        <div class="page" id="page1">
            <div class="title">
                <a href="/article1.htm">Page 1</a>
            </div>
            <div class="title">
                <a href="/article2.htm">Page 2</a>
            </div>
            <div class="title">
                <a href="/article3.htm">Page 3</a>
            </div>
            <a class="prev page1 disabled" href="#page1">prev</a>
            <a class="next page1" href="#page2">next</a>
        </div>
    </div>
    <div class="pageNav">
        <a class="pageNumber" href="#page1">1</a>
        <a class="pageNumber" href="#page2">2</a>
        <a class="pageNumber" href="#page3">3</a>
    </div>
</div>

Specifically what we’ve done here is add previous and next buttons to each page. The reason these have been added here, and not with the rest of the navigation buttons is because without javascript enabled, we can’t properly style or control the behavior of them dynamically, so we need static but functional links that we’ll only render on our mobile view. Functionally, at smaller screen sizes (like smart phones), requiring the user to click on one of several small links next to other small links can be frustrating, and giving them the same pagination functionality, but restricting them to only previous and next navigation is a simple process that drastically improves user experience. For all of these changes, we’ll need to update our css:

<style type="text/css">
.cssPagination:not(.js) .pages > .page:target ~ .page:last-child,
.cssPagination:not(.js) .pages > .page {display: none;}
.cssPagination:not(.js) .pages > :last-child,
.cssPagination:not(.js) .pages > .page:target {display: block;}
.cssPagination .prev, .next {display: none;}
.cssPagination.js .page:not(.paginationActive) {display: none;}
.cssPagination.js .page.paginationActive {display: block;}
.cssPagination.js .pageNav .pageNumber.paginationActive {font-weight: bold;}
@media screen and (max-width: 480px) {
    .cssPagination .prev, .next {display: block;width: 45%;}
    .cssPagination .prev {float: left;text-align: right;}
    .cssPagination .next {float: right;text-align: left;}
    .cssPagination:not(.js) .pageNav {display: none;}
    .cssPagination.js .pageNav .pageNumber {display: none;}
}
</style>

You’ll notice several new lines, so lets break them down.

Lastly we’ve added a script section, which will take over pagination when scripts are enabled (and in our case when jQuery is present):

<script type="text/javascript">
var checkReady = function (callback) {
    if (window.jQuery) {
        callback(jQuery);
    }
    else {
        window.setTimeout(function () { checkReady(callback); }, 100);
    }
};

checkReady(function ($) {
    // Use $ here...
    $(document).ready(function () {
        if ($(".cssPagination .pages .page").length > 1) {
            $(".cssPagination .page:visible").addClass("paginationActive");
            $(".cssPagination .pageNumber[href=#" + $(".page:visible").attr("id") + "]").addClass("paginationActive");
            $(".cssPagination .page:last-child").insertBefore(".cssPagination .page:first-child");
            $(".cssPagination .pageNumber").click(function (e) {
                //e.preventDefault();
                if (!$(this).hasClass("paginationActive")) {
                    $(".cssPagination .pageNumber.paginationActive, .cssPagination .page.paginationActive").removeClass("paginationActive");
                    $(".cssPagination .page" + $(this).attr("href")).addClass("paginationActive");
                    $(this).addClass("paginationActive");
                }
            });
            var prevHTML = $(".cssPagination .prev").html();
            var nextHTML = $(".cssPagination .next").html();
            $(".cssPagination .prev, .cssPagination .next").remove();
            $(".cssPagination .pageNav").prepend("<a href='" + $(".cssPagination .pageNumber.paginationActive").prev().attr("href") + "' class='prev'>" + prevHTML + "</a>");
            $(".cssPagination .prev").show().click(function (e) {
                //e.preventDefault();
                if ($(".cssPagination .page:visible").prev(".page").length > 0) {
                    $(".cssPagination .next").attr("href", $(".cssPagination .pageNumber.paginationActive").next().attr("href"));
                    $(".cssPagination .prev").attr("href", $(".cssPagination .pageNumber.paginationActive").prev().attr("href"));
                    var prev = $(".cssPagination .page:visible").prev(".page");
                    $(".cssPagination .next").removeClass("disabled");
                    $(".cssPagination .pageNumber.paginationActive, .cssPagination .page.paginationActive").removeClass("paginationActive");
                    $(prev).addClass("paginationActive");
                    $(".cssPagination .pageNumber[href=#" + $(".page:visible").attr("id") + "]").addClass("paginationActive");
                    if (!$(".cssPagination .page:visible").prev(".page").length > 0) {
                        $(this).addClass("disabled");
                    }
                }
            });
            $(".cssPagination .pageNav").append("<a href='" + $(".cssPagination .pageNumber.paginationActive").next().attr("href") + "' class='next'>" + nextHTML + "</a>");
            $(".cssPagination .next").show().click(function (e) {
                //e.preventDefault();
                if ($(".cssPagination .page:visible").next(".page").length > 0) {
                    $(".cssPagination .next").attr("href", $(".cssPagination .pageNumber.paginationActive").next().attr("href"));
                    $(".cssPagination .prev").attr("href", $(".cssPagination .pageNumber.paginationActive").prev().attr("href"));
                    var next = $(".cssPagination .page:visible").next(".page");
                    $(".cssPagination .prev").removeClass("disabled");
                    $(".cssPagination .pageNumber.paginationActive, .cssPagination .page.paginationActive").removeClass("paginationActive");
                    $(next).addClass("paginationActive");
                    $(".cssPagination .pageNumber[href=#" + $(".page:visible").attr("id") + "]").addClass("paginationActive");
                    if (!$(".cssPagination .page:visible").next(".page").length > 0) {
                        $(this).addClass("disabled");
                    }
                }
            });

            $(".cssPagination").addClass("js");
        }
    });
});
</script>

To see this all together in action click here. To see how this setup looks without javascript enabled all you need to do is set addInjQuery at the top of the script file to false. The demo also demonstrates how the previous and next buttons respond in a responsive manner to the available screen resolution both with and without the script enabled.

NOTE: To use this script as is, I recommend using the version shown here and not the one in the demo, as it is hard-coded to a specific version of jQuery, rather than this version which is compatible with virtually any version of jQuery that you might already have on your site.

The script that was added now only works when scripts are enabled and jQuery is available. This allows the content to be enhanced progressively depending on what features are or aren’t enabled on your user’s browser and device. This ensures that no matter what device or browser your client is using they will get the best possible experience from your site in the most efficient manner possible.

To recap this method for how to paginate a listing on a web page is responsive, not dependant on javascript, not dependant on posts to the server (either synchronous or asynchronous), 508 compliant, and in most cases incredibly efficient. It is also heavily customizable to fit in any website design in any language.

That’s all for this topic. To read more by some of our other incredibly talented staff see below: