JAVASCRIPTshopifyintermediate
Shopify Infinite Scroll for Collections
Add infinite scroll pagination to Shopify collection pages
Faisal Yaqoob
November 18, 2025
#shopify#infinite-scroll#pagination#ajax#collections
Code
javascript
1 // Infinite Scroll Class 2 class InfiniteScroll { 3 constructor(options = {}) { 4 this.container = options.container || '[data-products-container]'; 5 this.pagination = options.pagination || '[data-pagination]'; 6 this.loading = options.loading || '[data-loading]'; 7 this.offset = options.offset || 200; 8
9 this.isLoading = false; 10 this.hasMore = true; 11 this.currentPage = 1; 12
13 this.init(); 14 } 15
16 init() { 17 this.containerEl = document.querySelector(this.container); 18 this.paginationEl = document.querySelector(this.pagination); 19 this.loadingEl = document.querySelector(this.loading); 20
21 if (!this.containerEl) { 22 console.error('Container not found'); 23 return; 24 } 25
26 // Hide default pagination 27 if (this.paginationEl) { 28 this.paginationEl.style.display = 'none'; 29 } 30
31 // Get total pages from pagination 32 this.getTotalPages(); 33
34 // Bind scroll event 35 window.addEventListener('scroll', () => this.onScroll()); 36 } 37
38 getTotalPages() { 39 const lastPageLink = this.paginationEl?.querySelector('a:last-of-type'); 40
41 if (lastPageLink) { 42 const url = new URL(lastPageLink.href); 43 this.totalPages = parseInt(url.searchParams.get('page')) || 1; 44 } else { 45 this.totalPages = 1; 46 } 47
48 this.hasMore = this.currentPage < this.totalPages; 49 } 50
51 onScroll() { 52 if (this.isLoading || !this.hasMore) return; 53
54 const scrollPosition = window.scrollY + window.innerHeight; 55 const documentHeight = document.documentElement.scrollHeight; 56
57 if (scrollPosition >= documentHeight - this.offset) { 58 this.loadMore(); 59 } 60 } 61
62 async loadMore() { 63 if (this.isLoading || !this.hasMore) return; 64
65 this.isLoading = true; 66 this.showLoading(); 67
68 this.currentPage++; 69
70 try { 71 const url = new URL(window.location.href); 72 url.searchParams.set('page', this.currentPage); 73
74 const response = await fetch(url.toString()); 75 const html = await response.text(); 76
77 const parser = new DOMParser(); 78 const doc = parser.parseFromString(html, 'text/html'); 79
80 const newProducts = doc.querySelectorAll('[data-product-item]'); 81
82 if (newProducts.length > 0) { 83 newProducts.forEach(product => { 84 this.containerEl.appendChild(product.cloneNode(true)); 85 }); 86
87 // Trigger custom event for analytics or other scripts 88 document.dispatchEvent(new CustomEvent('infiniteScroll:loaded', { 89 detail: { 90 page: this.currentPage, 91 productsLoaded: newProducts.length 92 } 93 })); 94 } 95
96 // Check if there are more pages 97 if (this.currentPage >= this.totalPages) { 98 this.hasMore = false; 99 this.showEndMessage(); 100 } 101
102 } catch (error) { 103 console.error('Error loading more products:', error); 104 this.showError(); 105 } finally { 106 this.isLoading = false; 107 this.hideLoading(); 108 } 109 } 110
111 showLoading() { 112 if (this.loadingEl) { 113 this.loadingEl.style.display = 'block'; 114 } 115 } 116
117 hideLoading() { 118 if (this.loadingEl) { 119 this.loadingEl.style.display = 'none'; 120 } 121 } 122
123 showEndMessage() { 124 const message = document.createElement('div'); 125 message.className = 'infinite-scroll-end'; 126 message.innerHTML = '<p>You've reached the end of the collection</p>'; 127 this.containerEl.parentElement.appendChild(message); 128 } 129
130 showError() { 131 const error = document.createElement('div'); 132 error.className = 'infinite-scroll-error'; 133 error.innerHTML = ` 134 <p>Failed to load more products</p> 135 <button onclick="location.reload()">Retry</button> 136 `; 137 this.containerEl.parentElement.appendChild(error); 138 } 139 } 140
141 // Initialize on page load 142 document.addEventListener('DOMContentLoaded', () => { 143 const infiniteScroll = new InfiniteScroll({ 144 container: '[data-products-container]', 145 pagination: '[data-pagination]', 146 loading: '[data-loading]', 147 offset: 300 // Trigger 300px before bottom 148 }); 149 });
Shopify Infinite Scroll for Collections
Implement infinite scroll pagination on collection pages, automatically loading more products as users scroll to the bottom of the page.
// Infinite Scroll Class
class InfiniteScroll {
constructor(options = {}) {
this.container = options.container || '[data-products-container]';
this.pagination = options.pagination || '[data-pagination]';
this.loading = options.loading || '[data-loading]';
this.offset = options.offset || 200;
this.isLoading = false;
this.hasMore = true;
this.currentPage = 1;
this.init();
}
init() {
this.containerEl = document.querySelector(this.container);
this.paginationEl = document.querySelector(this.pagination);
this.loadingEl = document.querySelector(this.loading);
if (!this.containerEl) {
console.error('Container not found');
return;
}
// Hide default pagination
if (this.paginationEl) {
this.paginationEl.style.display = 'none';
}
// Get total pages from pagination
this.getTotalPages();
// Bind scroll event
window.addEventListener('scroll', () => this.onScroll());
}
getTotalPages() {
const lastPageLink = this.paginationEl?.querySelector('a:last-of-type');
if (lastPageLink) {
const url = new URL(lastPageLink.href);
this.totalPages = parseInt(url.searchParams.get('page')) || 1;
} else {
this.totalPages = 1;
}
this.hasMore = this.currentPage < this.totalPages;
}
onScroll() {
if (this.isLoading || !this.hasMore) return;
const scrollPosition = window.scrollY + window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
if (scrollPosition >= documentHeight - this.offset) {
this.loadMore();
}
}
async loadMore() {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
this.showLoading();
this.currentPage++;
try {
const url = new URL(window.location.href);
url.searchParams.set('page', this.currentPage);
const response = await fetch(url.toString());
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newProducts = doc.querySelectorAll('[data-product-item]');
if (newProducts.length > 0) {
newProducts.forEach(product => {
this.containerEl.appendChild(product.cloneNode(true));
});
// Trigger custom event for analytics or other scripts
document.dispatchEvent(new CustomEvent('infiniteScroll:loaded', {
detail: {
page: this.currentPage,
productsLoaded: newProducts.length
}
}));
}
// Check if there are more pages
if (this.currentPage >= this.totalPages) {
this.hasMore = false;
this.showEndMessage();
}
} catch (error) {
console.error('Error loading more products:', error);
this.showError();
} finally {
this.isLoading = false;
this.hideLoading();
}
}
showLoading() {
if (this.loadingEl) {
this.loadingEl.style.display = 'block';
}
}
hideLoading() {
if (this.loadingEl) {
this.loadingEl.style.display = 'none';
}
}
showEndMessage() {
const message = document.createElement('div');
message.className = 'infinite-scroll-end';
message.innerHTML = '<p>You've reached the end of the collection</p>';
this.containerEl.parentElement.appendChild(message);
}
showError() {
const error = document.createElement('div');
error.className = 'infinite-scroll-error';
error.innerHTML = `
<p>Failed to load more products</p>
<button onclick="location.reload()">Retry</button>
`;
this.containerEl.parentElement.appendChild(error);
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
const infiniteScroll = new InfiniteScroll({
container: '[data-products-container]',
pagination: '[data-pagination]',
loading: '[data-loading]',
offset: 300 // Trigger 300px before bottom
});
});
Liquid Template Structure
<!-- Collection Template -->
<div class="collection-products">
<div class="products-grid" data-products-container>
{% for product in collection.products %}
<div class="product-item" data-product-item>
<a href="{{ product.url }}">
{% if product.featured_image %}
<img src="{{ product.featured_image | img_url: '400x' }}" alt="{{ product.title }}">
{% endif %}
<h3 class="product-title">{{ product.title }}</h3>
<div class="product-price">
{{ product.price | money }}
</div>
</a>
</div>
{% endfor %}
</div>
<!-- Loading Indicator -->
<div class="loading-spinner" data-loading style="display: none;">
<div class="spinner"></div>
<p>Loading more products...</p>
</div>
<!-- Default Pagination (will be hidden) -->
<div class="pagination" data-pagination>
{% if paginate.previous %}
<a href="{{ paginate.previous.url }}">Previous</a>
{% endif %}
{% for part in paginate.parts %}
{% if part.is_link %}
<a href="{{ part.url }}">{{ part.title }}</a>
{% else %}
<span class="current">{{ part.title }}</span>
{% endif %}
{% endfor %}
{% if paginate.next %}
<a href="{{ paginate.next.url }}">Next</a>
{% endif %}
</div>
</div>
CSS Styling
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 30px;
margin-bottom: 50px;
}
.product-item {
text-align: center;
}
.product-item img {
width: 100%;
height: auto;
margin-bottom: 15px;
}
.product-title {
font-size: 16px;
margin-bottom: 10px;
}
.product-price {
font-size: 18px;
font-weight: bold;
}
/* Loading Spinner */
.loading-spinner {
text-align: center;
padding: 40px 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #333;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* End Message */
.infinite-scroll-end {
text-align: center;
padding: 40px 0;
color: #666;
border-top: 1px solid #eee;
margin-top: 40px;
}
/* Error Message */
.infinite-scroll-error {
text-align: center;
padding: 40px 0;
color: #c00;
}
.infinite-scroll-error button {
margin-top: 15px;
padding: 10px 20px;
background: #333;
color: #fff;
border: none;
cursor: pointer;
}
Advanced: With Load More Button
// Hybrid approach: Show button after certain pages
class HybridInfiniteScroll extends InfiniteScroll {
constructor(options = {}) {
super(options);
this.autoScrollPages = options.autoScrollPages || 3;
this.loadMoreBtn = null;
}
init() {
super.init();
this.createLoadMoreButton();
}
createLoadMoreButton() {
this.loadMoreBtn = document.createElement('button');
this.loadMoreBtn.className = 'btn-load-more';
this.loadMoreBtn.textContent = 'Load More Products';
this.loadMoreBtn.style.display = 'none';
this.loadMoreBtn.addEventListener('click', () => this.loadMore());
this.containerEl.parentElement.appendChild(this.loadMoreBtn);
}
onScroll() {
// Only auto-scroll for first few pages
if (this.currentPage < this.autoScrollPages) {
super.onScroll();
} else {
// Show load more button instead
if (this.hasMore && !this.isLoading) {
this.loadMoreBtn.style.display = 'block';
}
}
}
async loadMore() {
this.loadMoreBtn.style.display = 'none';
await super.loadMore();
}
}
// Use hybrid version
document.addEventListener('DOMContentLoaded', () => {
new HybridInfiniteScroll({
autoScrollPages: 3, // Auto-scroll first 3 pages, then show button
});
});
Integration with Filters
// Reset infinite scroll when filters change
document.addEventListener('filter:changed', () => {
const infiniteScroll = new InfiniteScroll();
infiniteScroll.currentPage = 1;
infiniteScroll.hasMore = true;
infiniteScroll.getTotalPages();
});
Features
- Seamless Loading: Products load automatically on scroll
- Performance Optimized: Debounced scroll events
- Error Handling: Graceful failure with retry option
- Progress Indicator: Loading spinner while fetching
- End Detection: Shows message when all products loaded
- Hybrid Mode: Option to show "Load More" button
- Event System: Custom events for integration
- SEO Friendly: Maintains pagination URLs
Related Snippets
Shopify AJAX Collection Filters
Add dynamic filtering to collection pages without page reloads
JAVASCRIPTshopifyadvanced
javascriptPreview
// Collection Filters Class
class CollectionFilters {
constructor(options = {}) {
this.container = document.querySelector(options.container || '[data-collection-container]');
...#shopify#filters#ajax+2
11/27/2025
View
Shopify Search Autocomplete
Add predictive search with product suggestions and instant results
JAVASCRIPTshopifyintermediate
javascriptPreview
// Search Autocomplete Class
class SearchAutocomplete {
constructor(options = {}) {
this.searchInput = document.querySelector(options.input || '[data-search-input]');
...#shopify#search#autocomplete+2
11/21/2025
View
Shopify Product Quick View Modal
Add a quick view popup to preview products without leaving the page
JAVASCRIPTshopifyintermediate
javascriptPreview
// Quick View Modal Class
class ProductQuickView {
constructor() {
this.modal = null;
...#shopify#product#modal+2
11/17/2025
View