How to add Load more button on the collection page?

Topic summary

Goal: Add either infinite scroll or a “Load more” button to the Dawn theme collection page.

Implemented solutions:

  • Infinite scroll: Add Ajaxinate.min.js to assets, include it in main-collection-product-grid.liquid, and configure selectors (container and pagination) so products append as users scroll.
  • Load more button: Create collection-load-more.js to fetch paginate.next.url and append new .grid__item elements; replace the default pagination render with a custom Load More button + spinner; add a hidden/utility element exposing paginate.next.url (id paginateNext) and minimal CSS for centering.

Results/outcomes:

  • Original poster confirmed both infinite scroll and Load More worked. Multiple users reported success.

Issue reported (half items loaded):

  • Some users saw only half the expected products per click. One noted fetch returned page=2 but parsed DOM reflected page=1.
  • Workaround/fix: Use an alternative Ajaxinate implementation and update facets.js (Shopify filtering) to re-initialize Ajaxinate after filters change, ensuring correct page content loads. Full replacement code wasn’t fully shown.

Notes:

  • Code snippets and screenshots are central to implementation.
  • Current status: Largely resolved with the alternative Ajaxinate + facets.js re-init; original half-load root cause not definitively diagnosed in-thread.
Summarized with AI on December 12. AI used: gpt-5.

Hey, I need to add a “Load more” button below the collection page. I am using dawn theme. Can anyone help?

Thanks :slightly_smiling_face:

  • Here is the solution for you @leechoostore
  • Please follow these steps:

  • Find the asset folder and add a new file “Ajaxinate.min.js” with the following content:
'use strict';

var Ajaxinate = function ajaxinateConstructor(config) {
  var settings = config || {};
  /*
    pagination: Selector of pagination container
    method: [options are 'scroll', 'click']
    container: Selector of repeating content
    offset: 0, offset the number of pixels before the bottom to start loading more on scroll
    loadingText: 'Loading', The text changed during loading
    callback: null, function to callback after a new page is loaded
  */
  var defaultSettings = {
    pagination: '#AjaxinatePagination',
    method: 'scroll',
    container: '#AjaxinateLoop',
    offset: 0,
    loadingText: 'Loading',
    callback: null
  };
  // Merge configs
  this.settings = Object.assign(defaultSettings, settings);

  // Bind 'this' to applicable prototype functions
  this.addScrollListeners = this.addScrollListeners.bind(this);
  this.addClickListener = this.addClickListener.bind(this);
  this.checkIfPaginationInView = this.checkIfPaginationInView.bind(this);
  this.stopMultipleClicks = this.stopMultipleClicks.bind(this);
  this.destroy = this.destroy.bind(this);

  // Set up our element selectors
  this.containerElement = document.querySelector(this.settings.container);
  this.paginationElement = document.querySelector(this.settings.pagination);

  this.initialize();
};

Ajaxinate.prototype.initialize = function initializeTheCorrectFunctionsBasedOnTheMethod() {
  // Find and initialise the correct function based on the method set in the config
  if (this.containerElement) {
    var initializers = {
      click: this.addClickListener,
      scroll: this.addScrollListeners
    };
    initializers[this.settings.method]();
  }
};

Ajaxinate.prototype.addScrollListeners = function addEventListenersForScrolling() {
  if (this.paginationElement) {
    document.addEventListener('scroll', this.checkIfPaginationInView);
    window.addEventListener('resize', this.checkIfPaginationInView);
    window.addEventListener('orientationchange', this.checkIfPaginationInView);
  }
};

Ajaxinate.prototype.addClickListener = function addEventListenerForClicking() {
  if (this.paginationElement) {
    this.nextPageLinkElement = this.paginationElement.querySelector('a');
    this.clickActive = true;
    if (this.nextPageLinkElement !== null) {
      this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
    }
  }
};

Ajaxinate.prototype.stopMultipleClicks = function handleClickEvent(event) {
  event.preventDefault();
  if (this.clickActive) {
    this.nextPageLinkElement.innerHTML = this.settings.loadingText;
    this.nextPageUrl = this.nextPageLinkElement.href;
    this.clickActive = false;
    this.loadMore();
  }
};

Ajaxinate.prototype.checkIfPaginationInView = function handleScrollEvent() {
  var top = this.paginationElement.getBoundingClientRect().top - this.settings.offset;
  var bottom = this.paginationElement.getBoundingClientRect().bottom + this.settings.offset;
  if (top <= window.innerHeight && bottom >= 0) {
    this.nextPageLinkElement = this.paginationElement.querySelector('a');
    this.removeScrollListener();
    if (this.nextPageLinkElement) {
      this.nextPageLinkElement.innerHTML = this.settings.loadingText;
      this.nextPageUrl = this.nextPageLinkElement.href;
      this.loadMore();
    }
  }
};

Ajaxinate.prototype.loadMore = function getTheHtmlOfTheNextPageWithAnAjaxRequest() {
  this.request = new XMLHttpRequest();
  this.request.onreadystatechange = function success() {
    if (this.request.readyState === 4 && this.request.status === 200) {
      var newContainer = this.request.responseXML.querySelectorAll(this.settings.container)[0];
      var newPagination = this.request.responseXML.querySelectorAll(this.settings.pagination)[0];
      this.containerElement.insertAdjacentHTML('beforeend', newContainer.innerHTML);
      this.paginationElement.innerHTML = newPagination.innerHTML;
      if (this.settings.callback && typeof this.settings.callback === 'function') {
        this.settings.callback(this.request.responseXML);
      }
      this.initialize();
    }
  }.bind(this);
  this.request.open('GET', this.nextPageUrl);
  this.request.responseType = 'document';
  this.request.send();
};

Ajaxinate.prototype.removeClickListener = function removeClickEventListener() {
  this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
};

Ajaxinate.prototype.removeScrollListener = function removeScrollEventListener() {
  document.removeEventListener('scroll', this.checkIfPaginationInView);
  window.removeEventListener('resize', this.checkIfPaginationInView);
  window.removeEventListener('orientationchange', this.checkIfPaginationInView);
};

Ajaxinate.prototype.destroy = function removeEventListenersAndReturnThis() {
  // This method is used to unbind event listeners from the DOM
  // This function is called manually to destroy "this" Ajaxinate instance
  var destroyers = {
    click: this.removeClickListener,
    scroll: this.removeScrollListener
  };
  destroyers[this.settings.method]();
  return this;
};
  • Then add the following code to the main-collection-product-grid.liquid
{{ 'ajaxinate.min.js' | asset_url | script_tag }}
  • Add the following code before {% schema %} in main-collection-product-grid.liquid

  • Find the file pagination.liquid:

  • Search for {%- if paginate.next -%} and add class=“infinite_next” in

  • . So the code should look like below:

{%- if paginate.next -%}
          - {%- render 'icon-caret' -%}
            
          

        {%- endif -%}
  • Adding code in facet.js File

  • Search for static renderProductCount(html) and below that add the following code:

const endlessCollection = new Ajaxinate({
container: '#product-grid',
pagination: '.infinite_next',
});
  • Adding code in component-pagination.css File
.pagination__list{
  display: none;
}
  • Please press ‘Like’ and mark it as ‘Solution’ if you find it helpful. Thank you.
3 Likes

The above code will help you automatically load more products on the collection page when customers scroll to the bottom of the page

1 Like

@leechoostore if you want the “load more” button. follow the step

1 Like
  • Add this code to theme.liquid
{% if template contains 'collection' %}
  
{%endif%}

1 Like

Add file collection-load-more.js in assest with content

const products_on_page = document.getElementById('product-grid');
const nextUrl = document.getElementById('paginateNext');
let next_url = nextUrl.dataset.nextUrl;

const load_more_btn = document.getElementsByClassName('load-more_btn')[0];
const load_more_spinner = document.getElementsByClassName('load-more_spinner')[0];
async function getNextPage() {
  try {
    let res = await fetch(next_url);
    return await res.text();
  } catch (error) {
    console.log(error);
  }
}

async function loadMoreProducts() {
  load_more_btn.style.display = 'none';
  load_more_spinner.style.display = 'block';
  let nextPage = await getNextPage();

  const parser = new DOMParser();
  const nextPageDoc = parser.parseFromString(nextPage, 'text/html');

  load_more_spinner.style.display = 'none';

  const productgrid = nextPageDoc.getElementById('product-grid');
  const new_products = productgrid.getElementsByClassName('grid__item');
  const newUrl = document.getElementById('paginateNext');
  const new_url = newUrl.dataset.nextUrl;
  if (new_url) {
    load_more_btn.style.display = 'flex';
  }
  next_url = new_url;
  for (let i = 0; i < new_products.length; i++) {
    products_on_page.appendChild(new_products[i]);
  }
}

1 Like
  • In the file main-collection-product-grid find {%- if paginate.pages > 1 -%}

and replace

{%- if paginate.pages > 1 -%}
              {% render 'pagination', paginate: paginate, anchor: '' %}
{%- endif -%}

to

{%- if paginate.pages > 1 -%}
              
  Load More
  

            {%- endif -%}

1 Like
  • Add the code before div has id ProductGridContainer
{{paginate.next.url}}

like image

1 Like

Add this code to base.css

.load-more {
position: relative !important;
    justify-content: center !important;
    display: flex !important;
}
1 Like
  • Here is the result you will achieve:

1 Like

Please try to follow my instructions @leechoostore

1 Like

This worked perfectly for infinite scroll! Thank you so much!

This process worked perfectly for “Load more” button! Thanks a bunch!

1 Like

life saver, thank you!

This is great - really helpful, thank you.

I’m having a small issue which is the button is only loading half the number of items that the pagination is set to. I’m not a complete novice when it comes to javascript but I couldn’t see anything in the code that would be causing that, and when I revert to the shopify built-in pagination with pages, the correct number of products loads per page.

Any ideas what could be causing this? I haven’t edited your JS file, and my product grid liquid is below:

{{ 'template-collection.css' | asset_url | stylesheet_tag }}
{{ 'component-card.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}

{% if section.settings.image_shape == 'blob' %}
  {{ 'mask-blobs.css' | asset_url | stylesheet_tag }}
{%- endif -%}

{%- unless section.settings.quick_add == 'none' -%}
  {{ 'quick-add.css' | asset_url | stylesheet_tag }}
{%- endunless -%}

{{ 'custom__card.css' | asset_url | stylesheet_tag }}
{{ 'custom__collection.css' | asset_url | stylesheet_tag }}

{%- if section.settings.quick_add == 'standard' -%}
  
  
{%- endif -%}

{%- if section.settings.quick_add == 'bulk' -%}
  
  
  
  
{%- endif -%}

{%- style -%}
  .section-{{ section.id }}-padding {
    padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
    padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
  }

  @media screen and (min-width: 750px) {
    .section-{{ section.id }}-padding {
      padding-top: {{ section.settings.padding_top }}px;
      padding-bottom: {{ section.settings.padding_bottom }}px;
    }
  }
{%- endstyle -%}

  {%- paginate collection.products by section.settings.products_per_page -%}
    {% comment %} Sort is the first tabbable element when filter type is vertical {% endcomment %}
    {%- if section.settings.enable_sorting and section.settings.filter_type == 'vertical' -%}
      
    {%- endif -%}

    

      {{ 'component-facets.css' | asset_url | stylesheet_tag }}
      
      {%- if section.settings.enable_filtering or section.settings.enable_sorting -%}
        
          

            ## {{ collection.title }}
          

          
            {% if section.settings.show_description %}
              
            {% endif %}
            {% render 'facets',
              results: collection,
              enable_filtering: section.settings.enable_filtering,
              enable_sorting: section.settings.enable_sorting,
              filter_type: section.settings.filter_type,
              paginate: paginate
            %}
          

        
        {% if section.settings.show_description %}
          
            {{ collection.description }}
          

        {% endif %}
      {%- endif -%}

      {{ paginate.next.url }}

      
        {%- if collection.products.size == 0 -%}
          

            

            
              ## 
                {{ 'sections.collection_template.empty' | t -}}
                

                {{
                  'sections.collection_template.use_fewer_filters_html'
                  | t: link: collection.url, class: 'underlined-link link'
                }}
              
            

          

        {%- else -%}
          
            

            
              {% assign skip_card_product_styles = false %}
              {%- for product in collection.products -%}
                {% assign page_number = paginate.current_page | minus: 1 %}
                {% assign base_index = section.settings.products_per_page | times: page_number %}
                {% assign grid_index = base_index | plus: forloop.index %}
                {% assign lazy_load = false %}
                {%- if grid_index > 4 -%}
                  {%- assign lazy_load = true -%}
                {%- endif -%}
                {% for block in section.blocks %}
                  {% case block.type %}
                    {% when 'content' %}
                      {% if block.settings.index == grid_index %}
                        {% render 'grid-content-item',
                          span_horizontal: block.settings.horizontal_span,
                          span_vertical: block.settings.vertical_span,
                          forloop: forloop,
                          image: block.settings.image,
                          title: block.settings.title,
                          link: block.settings.link,
                          link_label: block.settings.link_label,
                          color_scheme: block.settings.color_scheme,
                          block_id: block.id
                        %}
                      {% endif %}
                  {% endcase %}
                {% endfor %}

                {% render 'grid-card-product',
                  forloop: forloop,
                  card_product: product,
                  media_aspect_ratio: section.settings.image_ratio,
                  image_shape: section.settings.image_shape,
                  show_secondary_image: section.settings.show_secondary_image,
                  show_vendor: section.settings.show_vendor,
                  show_rating: section.settings.show_rating,
                  lazy_load: lazy_load,
                  skip_styles: skip_card_product_styles,
                  quick_add: section.settings.quick_add,
                  section_id: section.id
                %}
                {%- assign skip_card_product_styles = true -%}
              {%- endfor -%}
            

            {%- if paginate.pages > 1 -%}
              
                Load More
                

              

            {%- endif -%}
          

        {%- endif -%}
      

    

  {%- endpaginate -%}
  {% if section.settings.image_shape == 'arch' %}
    {{ 'mask-arch.svg' | inline_asset_content }}
  {%- endif -%}

1 Like

I have also the same issue with you.

let nextPage = await getNextPage();
This fetches the exact next page(page=2), but after parsing to DOM
( const nextPageDOM = parser.parseFromString(nextPage, ‘text/html’);), the log of nextPageDOM shows that it is page=1, and display the half number of page=2. Really weird and hope you help me out, thanks.

I ended up using a different version of Ajaxinate that works for me.

"use strict";

var Ajaxinate = function ajaxinateConstructor(config) {
  var settings = config || {};
  /*
    pagination: Selector of pagination container
    method: [options are 'scroll', 'click']
    container: Selector of repeating content
    offset: 0, offset the number of pixels before the bottom to start loading more on scroll
    loadingText: 'Loading', The text changed during loading
    callback: null, function to callback after a new page is loaded
  */
  var defaultSettings = {
    pagination: "#AjaxinatePagination",
    method: "scroll",
    container: "#AjaxinateLoop",
    offset: 0,
    loadingText: "Loading",
    callback: null,
  };
  // Merge configs
  this.settings = Object.assign(defaultSettings, settings);

  // Bind 'this' to applicable prototype functions
  this.addScrollListeners = this.addScrollListeners.bind(this);
  this.addClickListener = this.addClickListener.bind(this);
  this.checkIfPaginationInView = this.checkIfPaginationInView.bind(this);
  this.stopMultipleClicks = this.stopMultipleClicks.bind(this);
  this.destroy = this.destroy.bind(this);

  // Set up our element selectors
  this.containerElement = document.querySelector(this.settings.container);
  this.paginationElement = document.querySelector(this.settings.pagination);

  this.initialize();
};

Ajaxinate.prototype.initialize =
  function initializeTheCorrectFunctionsBasedOnTheMethod() {
    // Find and initialise the correct function based on the method set in the config
    if (this.containerElement) {
      var initializers = {
        click: this.addClickListener,
        scroll: this.addScrollListeners,
      };
      initializers[this.settings.method]();
    }
  };

Ajaxinate.prototype.addScrollListeners =
  function addEventListenersForScrolling() {
    if (this.paginationElement) {
      document.addEventListener("scroll", this.checkIfPaginationInView);
      window.addEventListener("resize", this.checkIfPaginationInView);
      window.addEventListener(
        "orientationchange",
        this.checkIfPaginationInView
      );
    }
  };

Ajaxinate.prototype.addClickListener = function addEventListenerForClicking() {
  if (this.paginationElement) {
    this.nextPageLinkElement = this.paginationElement.querySelector("a");
    this.clickActive = true;
    if (this.nextPageLinkElement !== null) {
      this.nextPageLinkElement.addEventListener(
        "click",
        this.stopMultipleClicks
      );
    }
  }
};

Ajaxinate.prototype.stopMultipleClicks = function handleClickEvent(event) {
  event.preventDefault();
  if (this.clickActive) {
    this.nextPageLinkElement.innerHTML = this.settings.loadingText;
    this.nextPageUrl = this.nextPageLinkElement.href;
    this.clickActive = false;
    this.loadMore();
  }
};

Ajaxinate.prototype.checkIfPaginationInView = function handleScrollEvent() {
  var top =
    this.paginationElement.getBoundingClientRect().top - this.settings.offset;
  var bottom =
    this.paginationElement.getBoundingClientRect().bottom +
    this.settings.offset;
  if (top <= window.innerHeight && bottom >= 0) {
    this.nextPageLinkElement = this.paginationElement.querySelector("a");
    this.removeScrollListener();
    if (this.nextPageLinkElement) {
      this.nextPageLinkElement.innerHTML = this.settings.loadingText;
      this.nextPageUrl = this.nextPageLinkElement.href;
      this.loadMore();
    }
  }
};

Ajaxinate.prototype.loadMore = function loadMore() {
  this.request = new XMLHttpRequest();

  this.request.onreadystatechange = function success() {
    if (!this.request.responseXML) {
      return;
    }
    if (!this.request.readyState === 4 || !this.request.status === 200) {
      return;
    }

    var newContainer = this.request.responseXML.querySelectorAll(
      this.settings.container
    )[0];
    var newPagination = this.request.responseXML.querySelectorAll(
      this.settings.pagination
    )[0];

    this.containerElement.insertAdjacentHTML(
      "beforeend",
      newContainer.innerHTML
    );

    if (typeof newPagination === "undefined") {
      this.removePaginationElement();
    } else {
      this.paginationElement.innerHTML = newPagination.innerHTML;

      if (
        this.settings.callback &&
        typeof this.settings.callback === "function"
      ) {
        this.settings.callback(this.request.responseXML);
      }

      this.initialize();
    }
  }.bind(this);

  this.request.open("GET", this.nextPageUrl);
  this.request.responseType = "document";
  this.request.send();
};

Ajaxinate.prototype.removeClickListener = function removeClickEventListener() {
  this.nextPageLinkElement.addEventListener("click", this.stopMultipleClicks);
};

Ajaxinate.prototype.removePaginationElement =
  function removePaginationElement() {
    this.paginationElement.innerHTML = "";
    this.destroy();
  };

Ajaxinate.prototype.removeScrollListener =
  function removeScrollEventListener() {
    document.removeEventListener("scroll", this.checkIfPaginationInView);
    window.removeEventListener("resize", this.checkIfPaginationInView);
    window.removeEventListener(
      "orientationchange",
      this.checkIfPaginationInView
    );
  };

Ajaxinate.prototype.destroy = function removeEventListenersAndReturnThis() {
  // This method is used to unbind event listeners from the DOM
  // This function is called manually to destroy "this" Ajaxinate instance
  var destroyers = {
    click: this.removeClickListener,
    scroll: this.removeScrollListener,
  };
  destroyers[this.settings.method]();
  return this;
};

It’s also necessary to updates facets.js (assuming you’re on Dawn or another base Shopify template) to refire the ajaxinate when facets are applied or disapplied. This is done by replacing this function in facets.js:

static renderProductGridContainer(html) {
    document.getElementById('ProductGridContainer').innerHTML = new DOMParser()
      .parseFromString(html, 'text/html')
      .getElementById('ProductGridContainer').innerHTML;

    document
      .getElementById('ProductGridContainer')
      .querySelectorAll('.scroll-trigger')
      .forEach((element) => {
        element.classList.add('scroll-trigger--cancel');
      });
  }

with this:

static renderProductGridContainer(html) {
    document.getElementById("ProductGridContainer").innerHTML = new DOMParser()
      .parseFromString(html, "text/html")
      .getElementById("ProductGridContainer").innerHTML;

    document
      .getElementById("ProductGridContainer")
      .querySelectorAll(".scroll-trigger")
      .forEach((element) => {
        element.classList.add("scroll-trigger--cancel");
      });

    const endlessCollection = new Ajaxinate({
      container: "#product-grid",
      pagination: "#AjaxinatePagination",
      method: "click",
    });
  }

Obviously adjust the settings here as needed according to the Ajaxinate settings you want to use.

Then on my main collection page I have this for the button:

{%- if paginate.next -%}
  
    Load More
  

{%- endif -%}

and this script at the bottom of the section:


(Again obviously adjust settings as needed). Hope that helps!