JAVASCRIPTshopifyintermediate

Shopify Infinite Scroll for Collections

Add infinite scroll pagination to Shopify collection pages

#shopify#infinite-scroll#pagination#ajax#collections
Share this snippet:

Code

javascript
1// Infinite Scroll Class
2class 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
142document.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