Infinite scroll in pure JavaScript for Ghost or WordPress blog

How to create a simple infinite-scroll mechanism? This article describes how infinite scroll is implemented on this blog.

Infinite scroll in pure JavaScript for Ghost or WordPress blog

This blog used to be based on WordPress. Although there are loads of ready-to-use WordPress plugins I tried not to use them if something is relatively easy to implement. Why? Because WordPress plugins are the main source of security vulnerabilities. Above all, this goes for not very popular ones.

I migrated to Ghost and it seems that the mechanism that I had created previously still works. Although this article shows an example for WordPress, it should work fine on Ghost too.

Main concept

Here is the list of key features:

  • It is written in pure JavaScript (no external dependencies).
  • In order to load older posts, the user needs to click “Load more” button. Another option would be to load posts automatically if the user reaches the bottom of the page. I decided to use the button for this but feel free to adjust it to your needs.
  • Under the hood, it replaces built-in navigation.
  • CSS classes’ names of generated HTML markup are configurable. However, it’s your responsibility to create styles for that.
  • Infinite scrolls works (or at least should work) on all WordPress pages like the main page, archives, search result and others.

The operating principle is quite simple. Both WordPress and Ghost provide a basic navigation mechanism with “Older posts” and “Newer posts” links. My script replaces navigation HTML with the “Load more” button. If the user clicks this button AJAX request for the next page is sent to the server. We take the URL of this request from the "Older posts" link before removing navigation from the DOM. The server returns HTML code for a page containing older posts. Then we crop out HTML elements, which means DIVs (of even better articles) with posts’ headers and texts, and put them to the current page. After that, we again convert navigation from returned page to the “Load more” button.

Infinite scroll diagram

It may sound a little bit weird to expect HTML code when using AJAX. We could use also WordPress rest API to load posts. Unfortunately, it would require more effort and it is not doable with simple JavaScript code. This solution is sufficient for a simple blog and supports most scenarios out of the box.


The script starts with configuration JSON. It lets us configure selectors of all HTML elements that are controlled by this mechanism. In addition to this, it’s possible to specify classes’ names for "load more section" elements, which are generated at runtime. You should adjust it to your website.

var infiniteScrollConfig = {    /* selectors */    articlesContainerId: 'br-articles-container',    articleSelector: '#br-articles-container article',    navigationSelector: '.br-common-navigation',    nextPageLinkSelector: '.nav-previous > a',
    /* custom classes */    loadMoreContainerClass: 'br-load-more',    buttonClass: 'br-load-more__button',    loaderClass: 'br-load-more__loader',    errorInfoClass: 'br-load-more__error',
    /* texts */    buttonText: 'Load more',    errorInfoText: 'Something went wrong! Please reload the page.',};...

This configuration is later passed together with document to IIFE on the bottom of the script.

...        }    }}(document, infiniteScrollConfig));

Let me also explain in brief the purpose of the needed selectors.

  • articlesContainerId is an id of an element that contains all articles (posts). This is typically main or div element. In my case it looks like this:
    infinite scroll - code 1
  • articleSelector as the name says is a selector of elements that contain bodies of articles. It typically contains the post’s header, metadata, and first paragraph or information about what is it about. These elements are usually direct children of containers element.
  • navigationSelector is a selector of WordPress navigation. It’s usually nav HTML element that contains "Older posts" and "Newer posts" links.
    infinite scroll - code 2
  • nextPageLinkSelector specifies anchor element with "Older posts" link.

Helper classes and functions

Subsequently, there is a beginning of IIFE. We’ve got some classes and functions that are later used in the infinite scroll mechanism.

To clarify, “classes” mean function constructors that are wrappers for HTML elements generated at runtime. LoadMoreContainer is a parent element for “Load more” button. It may also contain a loader if the request is in progress, or error message if occurred.

...(function(document, config) {    function LoadMoreContainer(handleLoadMore) {        var element = document.createElement('div');        element.classList.add(config.loadMoreContainerClass);
        var button = new Button(handleLoadMore);        element.appendChild(button.htmlElement);
        this.setLoading = function() {            element.innerHTML = '';            var loader = new Loader();            element.appendChild(loader.htmlElement);        }
        this.setError = function() {            element.innerHTML = '';            var errorInfo = new ErrorInfo();            element.appendChild(errorInfo.htmlElement);        }
        this.remove = function() {            element.parentElement.removeChild(element);        }
        this.htmlElement = element;
        function Button(clickHandler) {            var btn = document.createElement('button');            btn.classList.add(config.buttonClass);            btn.textContent = config.buttonText;            btn.addEventListener('click', clickHandler, false);            this.htmlElement = btn;        }
        function Loader() {            var loader = document.createElement('div');            loader.classList.add(config.loaderClass);            loader.setAttribute('aria-busy', 'true');            loader.setAttribute('role', 'alert');            this.htmlElement = loader;        }

        function ErrorInfo() {            var errorSpan = document.createElement('span');            errorSpan.classList.add(config.errorInfoClass);            errorSpan.textContent = config.errorInfoText;            this.htmlElement = errorSpan;        }    }    ...

There are also some helper functions that return the navigation elements or URL for the next page.

function getNavigation(htmlElement) {    if (!htmlElement) {        return null;    }
    return htmlElement.querySelector(config.navigationSelector);}
function getNavigationLink(htmlElement) {    if (!htmlElement) {        return null;    }
    var navigationNextLink = htmlElement.querySelector(        config.nextPageLinkSelector    );
    if (navigationNextLink) {        return navigationNextLink.getAttribute('href');    }
    return null;}

Infinite scroll mechanism

Then the most important code begins. We grab needed HTML elements like content and navigation. getNavigationLink function returns "Older posts" link URL that will be used later. If all required pieces of the puzzles are available we initialize infinite scroll.

First of all, built-in navigation is removed, and “Load more container” is rendered on the page. We pass click handler (handleLoadMore) to the created container (it’s used by a button). Then, when the user clicks the button we change its state to “loading” so Loader appears. The script invokes AJAX request using XMLHttpRequest. When request result is available we remove the current container, take new articles from response HTML, append them to the current page and initialize new “Load more container”. In case of an error, we create the Error element that contains a message specified in configuration JSON.

var content = document.getElementById(config.articlesContainerId);var navigation = getNavigation(content);var nextUrl = getNavigationLink(navigation);
if (content && navigation && nextUrl) {    navigation.parentElement.removeChild(navigation);    appendLoadMoreContainer();
    function appendLoadMoreContainer() {        var container = new LoadMoreContainer(handleLoadMore);        content.appendChild(container.htmlElement);
        function handleLoadMore() {            container.setLoading();
            var request = new XMLHttpRequest();            request.onreadystatechange = function () {                if (this.readyState === XMLHttpRequest.DONE) {                    if (this.status >= 200 && this.status < 400) {                        container.remove();
                        var responseHtml = document.createElement('html');                        responseHtml.innerHTML = this.responseText;
                        var articles = responseHtml.querySelectorAll(                            config.articleSelector                        );
                        for (var i = 0; i < articles.length; i++) {                            content.appendChild(articles[i]);                        }
                        nextUrl = getNavigationLink(responseHtml);
                        if (nextUrl) {                            appendLoadMoreContainer();                        }                    } else {                        container.setError();                    }                }            };
  'GET', nextUrl, true);            request.send();        }    }}

Generated HTML markup

There are few states of “Load more container”. You should write your own CSS styles for each of them. Of course, class names (and texts) will be different according to your configuration.

// Button visible<div class="br-load-more">    <button class="br-load-more__button">Load more</button></div>
// Request in progress (loader)<div class="br-load-more">    <div class="br-load-more__loader" aria-busy="true" role="alert"></div></div>
// Error<div class="br-load-more">    <span class="br-load-more__error">An error has occurred. Please reload the page.</span></div>

You can find the final script here.


This simple script can make WordPress or Ghost blog a little bit more convenient to use. It does not require any external libraries. As it comes to WordPress blog, besides the main page, it should handle (depending on your theme) other pages like search results, archives, tags, etc. However, it may require some more adjustments from your side.

Tagged: Front-end


The comments system is based on GitHub Issues API. Once you add your comment to the linked issue on my GitHub repository, it will show up below.