How To Add Pagination on Collection [Horizon Theme Shopify]

After following the steps in this guide, your Shopify collection page will support two display modes: Infinite Scroll and Numbered Pagination. Store admins can easily toggle between them, choose how many products show per page, and customize pagination styles – all without using external apps.

1st Step

Replace everything in your main-collection.liquid file with the following:

{% liquid
  # Onboarding: When no products are available, we show placeholder items
  assign products = ''
  if request.design_mode and shop.products_count == 0
    for i in (1..16)
      assign products = products | append: ' ,'
    endfor
    assign products = products | split: ','
  endif
%}

<script
  src="{{ 'results-list.js' | asset_url }}"
  type="module"
></script>


<script>
  // main-collection.js
document.addEventListener('DOMContentLoaded', () => {
  // Handle URL hash scrolling
  const url = new URL(window.location.href);
  if (url.hash) {
    const card = document.getElementById(url.hash.slice(1));
    if (card) {
      card.scrollIntoView({ behavior: 'instant' });
    }
  }

  // Infinite Scroll Logic (only if pagination is disabled)
  {% unless section.settings.enable_pagination %}
    const grid = document.querySelector('.product-grid');
    const pagination = document.querySelector('.pagination');
    let currentPage = {{ paginate.current_page | default: 1 }};
    let isLoading = false;

    const loadMoreProducts = async () => {
      if (isLoading) return;
      isLoading = true;

      const nextPage = currentPage + 1;
      const perPage = {{ section.settings.products_per_page | default: 500 }};
      const url = new URL(window.location.href);
      url.searchParams.set('page', nextPage);

      try {
        const response = await fetch(url);
        const html = await response.text();
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const newItems = doc.querySelectorAll('.product-grid__item');
        const newPagination = doc.querySelector('.pagination');

        if (newItems.length > 0) {
          grid.insertAdjacentHTML('beforeend', Array.from(newItems).map(item => item.outerHTML).join(''));
          currentPage = nextPage;
        }

        if (!newPagination || newPagination.querySelector('.pagination-link.next') === null) {
          window.removeEventListener('scroll', handleScroll);
        }
      } catch (error) {
        console.error('Error loading more products:', error);
      } finally {
        isLoading = false;
      }
    };

    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 200) {
        loadMoreProducts();
      }
    };

    if (grid && pagination) {
      pagination.style.display = 'none';
      window.addEventListener('scroll', handleScroll);
    }
  {% endunless %}
});
</script>
{% comment %} We always render this full-width, as the child blocks have width: page/full settings {% endcomment %}
<div class="section-background color-{{ section.settings.color_scheme }}"></div>
<results-list
  class="section product-grid-container color-{{ section.settings.color_scheme }}"
  style="--padding-block-start: {{ section.settings.padding-block-start }}px; --padding-block-end: {{ section.settings.padding-block-end }}px;"
  section-id="{{ section.id }}"
>
  {% render 'skip-to-content-link', href: '#ResultsList', text: 'accessibility.skip_to_results_list' %}

  <div class="collection-wrapper grid gap-style">
    {% content_for 'block',
      type: 'filters',
      id: 'filters',
      results: collection,
      results_size: collection.products_count
    %}

    {% assign per_page = section.settings.products_per_page | default: 24 %}

    {% if request.design_mode and shop.products_count == 0 %}
      {% if section.settings.enable_pagination %}
        {% paginate products by per_page %}
          {% capture children %}
            {% for product in products %}
              <li
                class="product-grid__item product-grid__item--{{ forloop.index0 }}"
                data-page="{{ paginate.current_page }}"
                data-product-id="{{ product.id }}"
                data-view-transition-id="{{ product.id }}"
                ref="cards[]"
              >
                {% content_for 'block', type: 'product-card', id: 'product-card', closest.product: product %}
              </li>
            {% endfor %}
          {% endcapture %}
          {% render 'product-grid', section: section, children: children, products: products, paginate: paginate %}
        {% endpaginate %}
      {% else %}
        {% capture children %}
          {% for product in products %}
            <li
              class="product-grid__item product-grid__item--{{ forloop.index0 }}"
              data-product-id="{{ product.id }}"
              data-view-transition-id="{{ product.id }}"
              ref="cards[]"
            >
              {% content_for 'block', type: 'product-card', id: 'product-card', closest.product: product %}
            </li>
          {% endfor %}
        {% endcapture %}
        {% render 'product-grid', section: section, children: children, products: products %}
      {% endif %}
    {% else %}
      {% if section.settings.enable_pagination %}
        {% paginate collection.products by per_page %}
          {% capture children %}
            {% for product in collection.products %}
              <li
                id="{{ section.id }}-{{ product.id }}"
                class="product-grid__item product-grid__item--{{ forloop.index0 }}"
                data-page="{{ paginate.current_page }}"
                data-product-id="{{ product.id }}"
                ref="cards[]"
              >
                {% # theme-check-disable %}
                {% content_for 'block', type: 'product-card', id: 'product-card', closest.product: product %}
                {% # theme-check-enable %}
              </li>
            {% endfor %}
          {% endcapture %}
          {% render 'product-grid',
            section: section,
            children: children,
            products: collection.products,
            paginate: paginate
          %}
        {% endpaginate %}
      {% else %}
        {% capture children %}
          {% for product in collection.products %}
            <li
              id="{{ section.id }}-{{ product.id }}"
              class="product-grid__item product-grid__item--{{ forloop.index0 }}"
              data-product-id="{{ product.id }}"
              ref="cards[]"
            >
              {% # theme-check-disable %}
              {% content_for 'block', type: 'product-card', id: 'product-card', closest.product: product %}
              {% # theme-check-enable %}
            </li>
          {% endfor %}
        {% endcapture %}
        {% render 'product-grid',
          section: section,
          children: children,
          products: collection.products
        %}
      {% endif %}
    {% endif %}
  </div>
</results-list>

{% stylesheet %}
  .main-collection-grid {
    grid-column: var(--grid-column--mobile);
    @media screen and (width >= 750px) {
      grid-column: var(--grid-column--desktop);
    }
  }

  .collection-wrapper {
    @media screen and (width >= 750px) {
      grid-template-columns:
        1fr repeat(
          var(--centered-column-number),
          minmax(0, calc((var(--page-width) - var(--page-margin) * 2) / var(--centered-column-number)))
        )
        1fr;
    }
  }

  .collection-wrapper:has(.facets-block-wrapper--full-width),
  .collection-wrapper:has(.collection-wrapper--full-width) {
    @media screen and (width >= 750px) {
      grid-column: 1 / -1;
      grid-template-columns:
        minmax(var(--page-margin), 1fr) repeat(
          var(--centered-column-number),
          minmax(0, calc((var(--page-width) - var(--page-margin) * 2) / var(--centered-column-number)))
        )
        minmax(var(--page-margin), 1fr);
    }
  }

  .collection-wrapper:has(.facets--vertical) .facets-block-wrapper--vertical:not(.hidden) ~ .main-collection-grid {
    @media screen and (width >= 750px) {
      grid-column: var(--facets-vertical-col-width) / var(--full-width-column-number);
    }
  }

  .collection-wrapper:has(.facets-block-wrapper--vertical:not(#filters-drawer)):has(.collection-wrapper--full-width) {
    @media screen and (width >= 750px) {
      grid-column: 1 / -1;
      grid-template-columns: 0fr repeat(var(--centered-column-number), minmax(0, 1fr)) 0fr;
    }
  }

  :is(.collection-wrapper--full-width, .collection-wrapper--full-width-on-mobile)
    [product-grid-view='default']
    .product-grid__item
    .group-block {
    @media screen and (width < 750px) {
      padding-inline-start: max(var(--padding-xs), var(--padding-inline-start));
      padding-inline-end: max(var(--padding-xs), var(--padding-inline-end));
    }
  }

  :is(.collection-wrapper--full-width, .collection-wrapper--full-width-on-mobile)
    [product-grid-view='mobile-single']
    .product-grid__item
    .group-block {
    @media screen and (width < 750px) {
      padding-inline-start: max(var(--padding-xs), var(--padding-inline-start));
      padding-inline-end: max(var(--padding-xs), var(--padding-inline-end));
    }
  }
{% endstylesheet %}

{% schema %}
{
  "name": "t:names.collection_container",
  "enabled_on": {
    "templates": ["collection"]
  },
  "settings": [
    {
      "type": "select",
      "id": "layout_type",
      "label": "t:settings.type",
      "options": [
        { "value": "grid", "label": "t:options.grid" },
        { "value": "organic", "label": "t:options.editorial" }
      ],
      "default": "grid"
    },
    {
      "type": "select",
      "id": "product_card_size",
      "label": "t:settings.card_size",
      "options": [
        { "value": "small", "label": "t:options.small" },
        { "value": "medium", "label": "t:options.medium" },
        { "value": "large", "label": "t:options.large" },
        { "value": "extra-large", "label": "t:options.extra_large" }
      ],
      "default": "medium",
      "visible_if": "{{ section.settings.layout_type == 'grid' }}"
    },
    {
      "type": "select",
      "id": "mobile_product_card_size",
      "label": "t:settings.mobile_card_size",
      "options": [
        { "value": "small", "label": "t:options.small" },
        { "value": "large", "label": "t:options.large" }
      ],
      "default": "small"
    },
    {
      "type": "header",
      "content": "t:content.layout"
    },
    {
      "type": "select",
      "id": "product_grid_width",
      "label": "t:settings.width",
      "options": [
        { "value": "centered", "label": "t:options.page" },
        { "value": "full-width", "label": "t:options.full" }
      ],
      "default": "centered"
    },
    {
      "type": "checkbox",
      "id": "full_width_on_mobile",
      "label": "t:settings.full_width_on_mobile",
      "default": true,
      "visible_if": "{{ section.settings.product_grid_width != 'full-width' }}"
    },
    {
      "type": "range",
      "id": "columns_gap_horizontal",
      "label": "t:settings.horizontal_gap",
      "min": 0,
      "max": 50,
      "step": 1,
      "unit": "px",
      "default": 16
    },
    {
      "type": "range",
      "id": "columns_gap_vertical",
      "label": "t:settings.vertical_gap",
      "min": 0,
      "max": 50,
      "step": 1,
      "unit": "px",
      "default": 16
    },
    {
      "type": "range",
      "id": "padding-inline-start",
      "label": "t:settings.left_padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 0
    },
    {
      "type": "range",
      "id": "padding-inline-end",
      "label": "t:settings.right_padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 0
    },
    {
      "type": "header",
      "content": "t:content.section_layout"
    },
    {
      "type": "color_scheme",
      "id": "color_scheme",
      "label": "t:settings.color_scheme",
      "default": "scheme-1"
    },
    {
      "type": "range",
      "id": "padding-block-start",
      "label": "t:settings.top_padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 8
    },
    {
      "type": "range",
      "id": "padding-block-end",
      "label": "t:settings.bottom_padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 8
    },
    {
      "type": "header",
      "content": "Pagination Settings"
    },
    {
      "type": "checkbox",
      "id": "enable_pagination",
      "label": "Enable Pagination",
      "default": true,
      "info": "When unchecked, infinite scroll will be used instead."
    },
{
  "type": "range",
  "id": "products_per_page",
  "label": "Products per page",
  "min": 0,
      "max": 50,
      "step": 1,
      "default": 10,
      "visible_if": "{{ section.settings.enable_pagination }}"
  },
  {
      "type": "color",
      "id": "link_colorr",
      "label": "Link color",
      "default": "#333",
      "visible_if": "{{ section.settings.enable_pagination }}"
    },
    {
      "type": "color",
      "id": "link_hover_colorr",
      "label": "Link Active Bg",
      "default": "#333",
      "visible_if": "{{ section.settings.enable_pagination }}"
    },
    {
      "type": "color",
      "id": "link_hover_colorr-color",
      "label": "Link Active Color",
      "default": "#fff",
      "visible_if": "{{ section.settings.enable_pagination }}"
    },
    {
      "type": "color",
      "id": "current_colorr",
      "label": "Link Hover Bg",
      "default": "#eee",
      "visible_if": "{{ section.settings.enable_pagination }}"
    }
  ],
  "presets": []
}
{% endschema %}

2nd Step

Replace everything in your product-grid.liquid file with the following:

{% doc %}
  This snippet is used to render the product grid on collection and search pages.

  @param {object} section - The section object
  @param {object} paginate - Pagination object (optional, only when pagination is enabled)
  @param {object} products - Array of product objects
  @param {string} [title] - Header of the collection or search results
  @param {string} children - List or grid of product cards
{% enddoc %}

{% capture product_card_size %}
  {% render 'util-product-grid-card-size' section: section %}
{% endcapture %}
{% assign product_card_size = product_card_size | strip %}

{% style %}
  @media (min-width: 750px) {
    {% case section.settings.layout_type %}
      {% when 'grid' %}
        .product-grid--{{ section.id }}:is(.product-grid--grid) {
          --product-grid-columns-desktop: repeat(auto-fill, minmax({{ product_card_size }}, 1fr));
        }
      {% when 'organic' %}
        {% assign large_span = 2 %}
        {% assign row_cycle = 3 %}
        {% assign product_cycle = row_cycle | times: 2 %}
        {% assign right_large_start_col = 3 %}
        .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 1) {
          grid-column: 1 / span {{ large_span }};
        }

        .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 2),
        .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 5) {
          align-self: end;
        }

        .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + {{ product_cycle }}) {
          grid-column: {{ right_large_start_col }} / span {{ large_span }};
        }

        .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) {
          --product-grid-columns-desktop: repeat(4, 1fr);
        }
    {% endcase %}

    {% case section.settings.product_card_size %}
      {% when 'extra-large' or 'large' %}
        @container product-grid (width < calc({{ product_card_size }} * 3 + {{ section.settings.columns_gap_horizontal }}px * 2)) {
          .product-grid--{{ section.id }}:is(.product-grid--grid) {
            --product-grid-columns-desktop: repeat(2, 1fr);
          }
        }
    {% endcase %}

    .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) {
      --product-grid-columns-desktop: repeat(auto-fill, minmax(6.25rem, 1fr));
    }

    .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) .product-grid-view-zoom-out--details {
      display: block;
    }

    .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) .product-grid__card {
      padding-inline-start: var(--zoom-out-padding-inline-start, 0);
      padding-inline-end: var(--zoom-out-padding-inline-end, 0);
      padding-block-start: var(--zoom-out-padding-block-start, 0);
      padding-block-end: var(--zoom-out-padding-block-end, 0);
    }
  }
{% endstyle %}

<div
  id="ResultsList"
  class="
    grid main-collection-grid
    {%- if section.settings.inherit_color_scheme == false %} color-{{ section.settings.color_scheme }}{% endif %}
    {%- if section.settings.product_grid_width == 'full-width' %} collection-wrapper--full-width{% endif %}
    {%- if section.settings.full_width_on_mobile == true %} collection-wrapper--full-width-on-mobile{% endif %}
    spacing-style
  "
  style="
    --grid--margin--mobile: 0{% if section.settings.product_grid_width == 'centered' and section.settings.full_width_on_mobile == false %} var(--margin-lg){% else %} 0{% endif %};
    --grid-column--desktop: var(--{% if section.settings.product_grid_width == 'centered' %}centered{% else %}full-width{% endif %});
    --grid-column--mobile: var(--{% if section.settings.full_width_on_mobile %}full-width{% else %}{% if section.settings.product_grid_width == 'centered' %}centered{% else %}full-width{% endif %}{% endif %});
    --padding-inline-start: {{ section.settings.padding-inline-start }}px;
    --padding-inline-end: {{ section.settings.padding-inline-end }}px;
  "
>
  <div
    style="
      grid-column: var(--full-width);
      --product-grid-gap-mobile: {{ section.settings.columns_gap_vertical | at_most: 12 }}px {{ section.settings.columns_gap_horizontal | at_most: 12 }}px;
      --product-grid-gap-desktop: {{ section.settings.columns_gap_vertical }}px {{ section.settings.columns_gap_horizontal }}px;
      container-type: inline-size;
      container-name: product-grid;
    "
  >
    {% if products.size == 0 %}
      <div class="main-collection-grid__empty">
        <h2 class="main-collection-grid__empty-title h2">
          {{ 'content.no_products_found' | t }}
        </h2>
        <p>
          {{ 'content.use_fewer_filters_html' | t: link: collection.url, class: 'main-collection-grid__empty-link' }}
        </p>
      </div>
    {% else %}
      {% if title %}
        <h4 class="main-collection-grid__title">{{ title }}</h4>
      {% endif %}

      <ul
        class="product-grid product-grid--{{ section.id }} product-grid--{{ section.settings.layout_type }} {% if section.settings.mobile_product_card_size == 'large' %} product-grid-mobile--large{% endif %}"
        product-grid-view="default"
        ref="grid"
        {% if section.settings.enable_pagination %}last-page="{{ paginate.pages }}"{% endif %}
        role="list"
        data-product-card-size="{{ section.settings.product_card_size }}"
      >
        {{ children }}
      </ul>

      {% if section.settings.enable_pagination and paginate.pages > 1 %}
        <div class="pagination">
          {% render 'pagination', paginate: paginate %}
        </div>
      {% endif %}
    {% endif %}
  </div>
</div>

{% comment %}
  This script is used to set the grid view on the product grid stored in sessionStorage. Keeping it here helps us prevent seeing the default state.
{% endcomment %}
{% unless request.design_mode %}
  <script>
    (() => {
      const grid = document.querySelector('.product-grid');

      if (grid) {
        const currentDevice = window.innerWidth >= 750 ? 'desktop' : 'mobile';
        const storedLayout = sessionStorage.getItem(`product-grid-view-${currentDevice}`);

        if (storedLayout) {
          const options = document.querySelectorAll(`input[type="radio"][name="grid"]`);

          grid.setAttribute('product-grid-view', storedLayout);

          for (const option of options) {
            option.checked = option.value === storedLayout;
          }
        }
      }
    })();
  </script>
{% endunless %}

{% stylesheet %}
  .product-grid {
    --product-grid-gap: var(--product-grid-gap-mobile);
    isolation: isolate;

    @media screen and (width >= 750px) {
      --product-grid-gap: var(--product-grid-gap-desktop);
    }
  }

  .product-grid slideshow-arrows .slideshow-control {
    display: none;

    @media screen and (width >= 750px) {
      display: grid;
    }
  }

  .main-collection-grid {
    padding: var(--grid--margin--mobile);

    @media screen and (width >= 750px) {
      padding: var(--padding-block-start) var(--padding-inline-end) var(--padding-block-end) var(--padding-inline-start);
    }
  }

  .main-collection-grid__empty {
    padding-block: var(--padding-6xl);
    padding-inline: var(--page-margin);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--padding-sm);
  }

  .main-collection-grid__empty-title {
    margin: 0;
  }

  .collection-wrapper--full-width .main-collection-grid__title {
    margin-left: var(--page-margin);
  }

  .collection-wrapper--full-width-on-mobile .main-collection-grid__title {
    @media screen and (width < 750px) {
      margin-left: var(--page-margin);
    }
  }
{% endstylesheet %}

3rd Step

Create a new snippet file called pagination.liquid and add the following code:

{% comment %}
  Custom Pagination Component
  Usage: {% render 'pagination', paginate: paginate %}
{% endcomment %}

{% if paginate.pages > 1 %}
  <nav class="pagination-wrapper" role="navigation" aria-label="Pagination">
    <ul class="pagination-list" role="list">
      {% if paginate.previous %}
        <li>
          <a href="{{ paginate.previous.url }}" class="pagination-link prev" aria-label="Previous">
            &larr; Prev
          </a>
        </li>
      {% endif %}

      {% for part in paginate.parts %}
        <li>
          {% if part.is_link %}
            <a href="{{ part.url }}" class="pagination-link">{{ part.title }}</a>
          {% else %}
            <span class="pagination-link current">{{ part.title }}</span>
          {% endif %}
        </li>
      {% endfor %}

      {% if paginate.next %}
        <li>
          <a href="{{ paginate.next.url }}" class="pagination-link next" aria-label="Next">
            Next &rarr;
          </a>
        </li>
      {% endif %}
    </ul>
  </nav>
{% endif %}
<style>
  .pagination-wrapper {
  margin-top: 2rem;
  text-align: center;
}

.pagination-list {
  display: inline-flex;
  list-style: none;
  gap: 0.5rem;
  padding: 0;
}

.pagination-link {
  padding: 0.5rem 0.75rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  text-decoration: none;
  color: {{ section.settings.link_colorr }};
  font-weight: 500;
}

.pagination-link.current {
  background-color: {{ section.settings.link_hover_colorr }};
  color: {{ section.settings.link_hover_colorr-color }};
  pointer-events: none;
}

.pagination-link:hover {
  background-color: {{ section.settings.current_colorr }};
}

</style>

Result

5/5 - (5 votes)

About

Leave a Comment

Your email address will not be published. Required fields are marked *