How to

How to implement a proper infinite scroll in WordPress

This concept of infinite scroll is based on an approach which removes content from the DOM as the user advances with scrolling in a certain direction. Thus, content is loaded and deleted dynamically when scrolling both up and down. The aim of this approach is to avoid excessive memory use due to a presence of an always growing number of DOM elements as the user scrolls.

Infinite scroll can be enabled on single post pages, archive pages, and even the search page.

In this documentation, we’ll use the term container to refer to the DOM element which houses a piece of content. The piece of content can either be a page containing a collection of articles or the content of a single post page.

#WordPress ­default URLs used for requests

It’s recommended to load content from URLs that are cache-friendly which implies:

  • No calls to .php files for dynamic generation of content.
  • No use of request query variables in order to determine the offset, number or type of articles to load as part of an AJAX request. The easiest example to relate to are the URLs that are native to WordPress. Here are examples of some popular use cases:
    • domain.com/page/*page-number* – to load articles of the main blog archive
    • domain.com/*category-term*/page/*page-number* – to load articles of a category term archive
    • domain.com/*author-name*/page/*page-number* – to load articles of author archives

#Custom URLs used for requests

Of course, one should not necessarily limit oneself to the collection of URLs offered out-of-the-box by WordPress. An alternative which might offer better control over the collection of articles to load while scrolling is represented by defining a custom set of rewrite rules that map to a custom URL structure.

Let’s explore this approach in more detail using the following complex scenario: infinite scroll is applied to an archive which loads articles from a variety of post types, filtered using terms of various custom taxonomies.

Now, for a more specific example, we’ll consider an online magazine. Post types shall be represented by types of content: entertainment and movies. Filters shall map to custom taxonomies: news and fun.

The first and most important aspect to have in mind is that the URL must be structured to cover all needs to send request details to the server, that would otherwise be sent via query variables (as would be the case in requests sent to admin-ajax.php). For the example above, we’ll identify the need to tell the server which type of content the user is interested in, as well as the categories in which the content falls into.

To cover these needs, we structure a URL of the following format:

domain.com/browse/*content-1*+*content-2*/*filter-1*+*filter-2*/page/*page-num*/

E.g. : domain.com/browse/entertainment/news/page/2 – request the entertainment articles from the news category at the offset determined by doubling the per-page number of articles to display.
domain.com/browse/entertainment+movies/news+fun/page/1/ – request the articles of both content types that fall into either one or both of the categories news and fun, starting at offset 0.

Now, at server side, we use the following rewrite rule (more rules might be needed, though, to cover all request scenarios):

add_rewrite_rule(
'^browse/(.*?)/(.*?)/page/([0-9]+)/?

Notice how we manage to extract all the information that we would have equivalently requested using admin-ajax.php and also the corresponding query parameters on the server side. All we have to do now on the server side is to register the custom WordPress query vars browse_content, post_types and content_categories via the query_vars action hook.

Generating the page content can be performed by loading the necessary templates over filter hooks such as template_include or archive_template. By parsing the information stored in the WordPress query vars we can extract all information in order to generate the custom WP query to fetch the articles.
Of course, one very important matter to consider is that the cache needs to be refreshed when new articles are posted, in order to keep the content of all these custom URL variants up to date.

#Initializations at page load

At page load, there are a series of checks and initializations performed. First off, it is determined whether infinite scroll must be enabled, based on the template used to render the content. In order to compute the base URL to which requests for new content are sent during scroll, it is also the template that is checked:

  • in case the current template is an archive page, a regex is employed to separate any existing query strings from the URL belonging to the content initially loaded.
    Out of the query parameters, the only one of interest would be the search parameter, for WordPress: s=<search -keyword>.The presence of this query parameter indicates that the current template is a search template and the search parameter will always be part of the request URL for loading new content (e.g. domain.com/page/3/?s=).
    All other parameters are discharged, in order to avoid breaking the cache unnecessarily.Another piece of information extracted from the URL at initial load is the index of the current archive page.This index is used in order to determine which is the next page to load as the user scrolls up. This is considered the first index and no content is loaded from page indexes that are lower than it.
    E.g. : Initial content loaded from: domain.com/category/page/5/ first index = 5
    next index = 6
    -> URL for next request is domain.com/category/page/6/
    If the user, at a certain point, scrolls back up, the content of pages 1 – 4 will not be further loaded, scrolling stops at page with index 5.
  • in case the current template is a single post page, the content during infinite scroll is loaded from the main archive, starting at index 1.
    E.g. : first index=0 next index = 1 -> URL for next request is domain.com/page/1/

The content received at initial page load is considered pinned. This piece of content, whether it is the body of a single post or a collection of articles from an archive page, shall not be removed while scrolling. The reasoning behind this approach is that the user might choose to scroll through the content using the scrollbar. This implies a faster traversal of the content, which might lead to reaching the top of the document body prior to triggering a request for the content removed from the DOM. At most 3 pages are always kept in the DOM, in addition to the pinned page: the page in view, the page preceding it and the page following it. The preceding and following page are intended as a “buffer”, to prevent halting the scroll action while waiting for new content to be received.
A record is kept at all moments of the index of the container in view. The index in view is not changed as long as a container in full view. Also, no new request is sent to the server in this situation.

Current page - infinite scroll in WordPress
Fig. 1 Current page is in full view, taking up the entire viewport

The first request for new content, as part of the infinite scroll mechanism, is sent upon the first user scroll action, after the initial page load.

#Loading and deleting content

At every scroll event, the position of each container is checked, in relation to the viewport. In case the current page does not take up the entire viewport, a check is performed in order to determine whether it is the preceding page, the following page or the initial page that has entered the viewport.

In case it is either the preceding or the following page, this is considered as a trigger for requesting new content from the server.
The trigger for loading a new page downwards is the moment that the following page enters the viewport.
The index of the new page to request is computed based on the index of the last page loaded downwards and concatenated to the base URL to which requests are sent (i.e. domain.com/page/).

Current container no longer in full view
Fig. 2 Current container no longer in full view, new request issued

Infinite scroll in WordPress
Fig. 3 Following page becomes current page, preceding page is deleted from DOM

For as long as the response has yet to be received from the server, all checks performed on scroll events are suppressed and no further request is issued.
As the user continues to scroll, the index of the container in view is only updated when the following page is in full view. It is at this moment that the preceding page is removed from the DOM.
It is also at this point in scrolling when the URL and title are changed in the browser window. If needed, the browser navigation history can also be updated by pushing a new state in order to record the page transitions for accessing later via the browser Back button.

Similarly, a request for loading a page upwards is triggered at the moment the preceding page enters the viewport. In case the index of the preceding page is not the first index, the index of the page to load is computed based on the index of the preceding page. Just like with loading content after the following page, no further checks are done as the user scrolls, until a response is received from the server.

One aspect to have in mind, in this case, is that scrolling downwards while waiting for a response with the new content would eventually take the user to the bottom of the document body. At this point, a spinning wheel or an indicator alike would signal the user that content is yet to arrive.
In contrast to this, when scrolling upwards, should the response from the server be delayed, the user will enter the container associated to the first index. At this point, in order to avoid confusion should the user later decide to scroll back down, any pending request should be dropped and new content should not be inserted in between the container associated to the first index and the container holding the preceding page.

Just like in the case of scrolling downwards, the moment that the preceding page takes up the entire viewport, the new page loaded upwards shall become the preceding page, the current page shall become the following page and the preceding page shall become the current page. The container that holds the former following page is removed from the DOM and the URL and title are updated in the browser window with the values associated with the new current page.

#Inserting new content in the DOM

In case new content is loaded downwards, it is appended to the document body within a new container.
The situation is a bit different for content loaded upwards. Inserting new content above the current scroll position would end up shifting all existing content underneath, thus moving the user viewpoint.
In order to prevent this from happening, upon receiving the new content from the server, the value of the scroll position is retained. The height of the container holding the new content is computed and the new scroll position results from the addition of the height of the new content to the current scroll position.

#Additional considerations

One aspect to have in mind is avoiding the situation in which the same article is present more than once in the containers stored in the DOM.
Such a situation can appear in the case in which a new article is published as the user scrolls. In this case, the content of the new pages loaded downwards would be shifted by one article and chances are that an article that has already been loaded is also present in the content newly received.
In order to circumvent this, the recommendation is to keep a list of the post IDs that are in the DOM at every moment and curate the new content prior to inserting it into the document body, removing the IDs that have already been loaded.
The same reasoning applies in case initially it is the single post template that is displayed, at the bottom of which content is loaded, starting with the first page of the main archive.

A strong recommendation is to populate the infinite scroll with browser events, in order to allow hooking additional functionality to the moment when new content is added to the document body, as well as to the moment the user passes from one container to another.
An example of usage for the hook that fires when new content is loaded into the document body is firing the functionality which refreshes ad units. This can also be moved to the hook which fires on page transitions, but loading all ad units when the new page is just about to enter the viewport might result in a bit of a lag in scrolling.
An example of usage for the hook that fires when the user transitions from the current page to the following page is sending hit requests for a new page view to Google Analytics.

#Important variables

There are a number of variables worth explaining:

Upwards load trigger and Downwards load trigger -­ it is across these two variables that checks must be made in order to determine whether new content needs to be loaded. One possible approach is to store within these variables the HTML object associated to the containers that hold the preceding page and the following page and use the Javascript function getBoundingClientRect() in order to determine the position of the respective HTML elements in relation to the viewport. These checks must occur at every scroll event.
It is considered that the upwards load trigger is active (page needs to be loaded before the preceding page) if the bottom of the previous container has entered the viewport.
Similarly, it is considered that the downwards load trigger is active (page needs to be loaded after the following page) if the top of the next container has entered the viewport.
By using the Javascript object returned by the function getBoundingClientRect(), the situations above translate into having the value of its top property less than the height of the window and its bottom property greater than or equal to 0.

First index­ – the index of the container that holds the content loaded initially (article of single post template or list of articles from an archive page), is initialized at page load and does not change value upon user scroll. For a single post template, the first index will always be 0, while for an archive page the first index shall be equal to the number of the page loaded initially. Requests for new content upwards during infinite scroll must not extend to an index lower than the value of this variable.

Previous index­ the index of the page held within the previous container. The index of the page to load upwards during infinite scroll shall be determined by subtracting 1 from the value of this variable. Its value is updated upon each receiving of new content upwards. Loading new content upwards is prevented when subtracting 1 from the value of this variable results in the value of the first index. Its value is incremented as pages are removed upwards.

Next index­ the index of the page held within the next container. Similar to the previous index, the next page to load is determined by adding 1 to the value of this variable. Its value is decremented as pages are removed downwards.

Index in view­ the index of the container that currently occupies the viewport. The value of this variable updates at the moment when roles are switched between the current page and the previous/following page, upon loading and deleting content from the document body. The value of this index is correlated to the URL and title of the browser window.

List of article IDs­ an array storing the IDs of the articles that are displayed in the document body at any time. This variable is initialized at page load with the ID of the article displayed in the single post page template or with the IDs of the articles listed in the archive template loaded. As new content is loaded, it is this list of IDs that must be checked in order to filter out any potential duplicate of an article that is already in the document body. The reverse applies to the situation in which pages are removed from the document body when the list of article IDs needs to reflect only the articles that remain in the DOM.

Flag indicator of a request in progress activated upon the initiation of a new page request, the flag must only be reset once the response has been received from the server and the content has been inserted into the document body.

, 'index.php?browse_content&post_types=$matches[1]' . '&content_categories=$matches[2] &paged=$matches[4]', 'top' );

Notice how we manage to extract all the information that we would have equivalently requested using admin-ajax.php and also the corresponding query parameters on the server side. All we have to do now on the server side is to register the custom WordPress query vars browse_content, post_types and content_categories via the query_vars action hook.

Generating the page content can be performed by loading the necessary templates over filter hooks such as template_include or archive_template. By parsing the information stored in the WordPress query vars we can extract all information in order to generate the custom WP query to fetch the articles.
Of course, one very important matter to consider is that the cache needs to be refreshed when new articles are posted, in order to keep the content of all these custom URL variants up to date.

#Initializations at page load

At page load, there are a series of checks and initializations performed. First off, it is determined whether infinite scroll must be enabled, based on the template used to render the content. In order to compute the base URL to which requests for new content are sent during scroll, it is also the template that is checked:

  • in case the current template is an archive page, a regex is employed to separate any existing query strings from the URL belonging to the content initially loaded.
    Out of the query parameters, the only one of interest would be the search parameter, for WordPress: s=<search -keyword>.The presence of this query parameter indicates that the current template is a search template and the search parameter will always be part of the request URL for loading new content (e.g. domain.com/page/3/?s=).
    All other parameters are discharged, in order to avoid breaking the cache unnecessarily.Another piece of information extracted from the URL at initial load is the index of the current archive page.This index is used in order to determine which is the next page to load as the user scrolls up. This is considered the first index and no content is loaded from page indexes that are lower than it.
    E.g. : Initial content loaded from: domain.com/category/page/5/ first index = 5
    next index = 6
    -> URL for next request is domain.com/category/page/6/
    If the user, at a certain point, scrolls back up, the content of pages 1 – 4 will not be further loaded, scrolling stops at page with index 5.
  • in case the current template is a single post page, the content during infinite scroll is loaded from the main archive, starting at index 1.
    E.g. : first index=0 next index = 1 -> URL for next request is domain.com/page/1/

The content received at initial page load is considered pinned. This piece of content, whether it is the body of a single post or a collection of articles from an archive page, shall not be removed while scrolling. The reasoning behind this approach is that the user might choose to scroll through the content using the scrollbar. This implies a faster traversal of the content, which might lead to reaching the top of the document body prior to triggering a request for the content removed from the DOM. At most 3 pages are always kept in the DOM, in addition to the pinned page: the page in view, the page preceding it and the page following it. The preceding and following page are intended as a “buffer”, to prevent halting the scroll action while waiting for new content to be received.
A record is kept at all moments of the index of the container in view. The index in view is not changed as long as a container in full view. Also, no new request is sent to the server in this situation.

Current page is in full view - Infinite scroll in WordPress
Fig. 1 Current page is in full view, taking up the entire viewport

The first request for new content, as part of the infinite scroll mechanism, is sent upon the first user scroll action, after the initial page load.

#Loading and deleting content

At every scroll event, the position of each container is checked, in relation to the viewport. In case the current page does not take up the entire viewport, a check is performed in order to determine whether it is the preceding page, the following page or the initial page that has entered the viewport.

In case it is either the preceding or the following page, this is considered as a trigger for requesting new content from the server.
The trigger for loading a new page downwards is the moment that the following page enters the viewport.
The index of the new page to request is computed based on the index of the last page loaded downwards and concatenated to the base URL to which requests are sent (i.e. domain.com/page/).

Current container no longer in full view - Infinite scroll in WordPress
Fig. 2 Current container no longer in full view, new request issued

Following page becomes current page - Infinite scroll in WordPress
Fig. 3 Following page becomes current page, preceding page is deleted from DOM

For as long as the response has yet to be received from the server, all checks performed on scroll events are suppressed and no further request is issued.
As the user continues to scroll, the index of the container in view is only updated when the following page is in full view. It is at this moment that the preceding page is removed from the DOM.
It is also at this point in scrolling when the URL and title are changed in the browser window. If needed, the browser navigation history can also be updated by pushing a new state in order to record the page transitions for accessing later via the browser Back button.

Similarly, a request for loading a page upwards is triggered at the moment the preceding page enters the viewport. In case the index of the preceding page is not the first index, the index of the page to load is computed based on the index of the preceding page. Just like with loading content after the following page, no further checks are done as the user scrolls, until a response is received from the server.

One aspect to have in mind, in this case, is that scrolling downwards while waiting for a response with the new content would eventually take the user to the bottom of the document body. At this point, a spinning wheel or an indicator alike would signal the user that content is yet to arrive.
In contrast to this, when scrolling upwards, should the response from the server be delayed, the user will enter the container associated to the first index. At this point, in order to avoid confusion should the user later decide to scroll back down, any pending request should be dropped and new content should not be inserted in between the container associated to the first index and the container holding the preceding page.

Just like in the case of scrolling downwards, the moment that the preceding page takes up the entire viewport, the new page loaded upwards shall become the preceding page, the current page shall become the following page and the preceding page shall become the current page. The container that holds the former following page is removed from the DOM and the URL and title are updated in the browser window with the values associated with the new current page.

#Inserting new content in the DOM

In case new content is loaded downwards, it is appended to the document body within a new container.
The situation is a bit different for content loaded upwards. Inserting new content above the current scroll position would end up shifting all existing content underneath, thus moving the user viewpoint.
In order to prevent this from happening, upon receiving the new content from the server, the value of the scroll position is retained. The height of the container holding the new content is computed and the new scroll position results from the addition of the height of the new content to the current scroll position.

#Additional considerations

One aspect to have in mind is avoiding the situation in which the same article is present more than once in the containers stored in the DOM.
Such a situation can appear in the case in which a new article is published as the user scrolls. In this case, the content of the new pages loaded downwards would be shifted by one article and chances are that an article that has already been loaded is also present in the content newly received.
In order to circumvent this, the recommendation is to keep a list of the post IDs that are in the DOM at every moment and curate the new content prior to inserting it into the document body, removing the IDs that have already been loaded.
The same reasoning applies in case initially it is the single post template that is displayed, at the bottom of which content is loaded, starting with the first page of the main archive.

A strong recommendation is to populate the infinite scroll with browser events, in order to allow hooking additional functionality to the moment when new content is added to the document body, as well as to the moment the user passes from one container to another.
An example of usage for the hook that fires when new content is loaded into the document body is firing the functionality which refreshes ad units. This can also be moved to the hook which fires on page transitions, but loading all ad units when the new page is just about to enter the viewport might result in a bit of a lag in scrolling.
An example of usage for the hook that fires when the user transitions from the current page to the following page is sending hit requests for a new page view to Google Analytics.

#Important variables

There are a number of variables worth explaining:

Upwards load trigger and Downwards load trigger -­ it is across these two variables that checks must be made in order to determine whether new content needs to be loaded. One possible approach is to store within these variables the HTML object associated to the containers that hold the preceding page and the following page and use the Javascript function getBoundingClientRect() in order to determine the position of the respective HTML elements in relation to the viewport. These checks must occur at every scroll event.
It is considered that the upwards load trigger is active (page needs to be loaded before the preceding page) if the bottom of the previous container has entered the viewport.
Similarly, it is considered that the downwards load trigger is active (page needs to be loaded after the following page) if the top of the next container has entered the viewport.
By using the Javascript object returned by the function getBoundingClientRect(), the situations above translate into having the value of its top property less than the height of the window and its bottom property greater than or equal to 0.

First index­ – the index of the container that holds the content loaded initially (article of single post template or list of articles from an archive page), is initialized at page load and does not change value upon user scroll. For a single post template, the first index will always be 0, while for an archive page the first index shall be equal to the number of the page loaded initially. Requests for new content upwards during infinite scroll must not extend to an index lower than the value of this variable.

Previous index­ the index of the page held within the previous container. The index of the page to load upwards during infinite scroll shall be determined by subtracting 1 from the value of this variable. Its value is updated upon each receiving of new content upwards. Loading new content upwards is prevented when subtracting 1 from the value of this variable results in the value of the first index. Its value is incremented as pages are removed upwards.

Next index­ the index of the page held within the next container. Similar to the previous index, the next page to load is determined by adding 1 to the value of this variable. Its value is decremented as pages are removed downwards.

Index in view­ the index of the container that currently occupies the viewport. The value of this variable updates at the moment when roles are switched between the current page and the previous/following page, upon loading and deleting content from the document body. The value of this index is correlated to the URL and title of the browser window.

List of article IDs­ an array storing the IDs of the articles that are displayed in the document body at any time. This variable is initialized at page load with the ID of the article displayed in the single post page template or with the IDs of the articles listed in the archive template loaded. As new content is loaded, it is this list of IDs that must be checked in order to filter out any potential duplicate of an article that is already in the document body. The reverse applies to the situation in which pages are removed from the document body when the list of article IDs needs to reflect only the articles that remain in the DOM.

Flag indicator of a request in progress activated upon the initiation of a new page request, the flag must only be reset once the response has been received from the server and the content has been inserted into the document body.

Smart Managed WordPress Hosting

Presslabs provides high-performance hosting and business intelligence for the WordPress sites you care about.

Signup to Presslabs Newsletter

Thanks you for subscribing!