JAVASCRIPTshopifyadvanced
Shopify AJAX Collection Filters
Add dynamic filtering to collection pages without page reloads
Faisal Yaqoob
November 27, 2025
#shopify#filters#ajax#collections#faceted-search
Code
javascript
1 // Collection Filters Class 2 class CollectionFilters { 3 constructor(options = {}) { 4 this.container = document.querySelector(options.container || '[data-collection-container]'); 5 this.filtersForm = document.querySelector(options.filtersForm || '[data-filters-form]'); 6 this.loadingEl = document.querySelector(options.loading || '[data-loading]'); 7
8 if (!this.container || !this.filtersForm) { 9 console.error('Required elements not found'); 10 return; 11 } 12
13 this.isLoading = false; 14 this.currentUrl = new URL(window.location.href); 15
16 this.init(); 17 } 18
19 init() { 20 // Handle filter changes 21 this.filtersForm.addEventListener('change', (e) => { 22 if (e.target.matches('[data-filter-input]')) { 23 this.applyFilters(); 24 } 25 }); 26
27 // Handle price range 28 const priceInputs = this.filtersForm.querySelectorAll('[data-price-input]'); 29 priceInputs.forEach(input => { 30 input.addEventListener('input', () => this.debounce(() => this.applyFilters(), 500)); 31 }); 32
33 // Handle sort change 34 const sortSelect = document.querySelector('[data-sort-select]'); 35 if (sortSelect) { 36 sortSelect.addEventListener('change', () => this.applyFilters()); 37 } 38
39 // Clear filters 40 const clearBtn = document.querySelector('[data-clear-filters]'); 41 if (clearBtn) { 42 clearBtn.addEventListener('click', () => this.clearFilters()); 43 } 44
45 // Initial active filters display 46 this.updateActiveFilters(); 47 } 48
49 async applyFilters() { 50 if (this.isLoading) return; 51
52 this.isLoading = true; 53 this.showLoading(); 54
55 // Build filter URL 56 const url = this.buildFilterUrl(); 57
58 try { 59 const response = await fetch(url); 60 const html = await response.text(); 61
62 const parser = new DOMParser(); 63 const doc = parser.parseFromString(html, 'text/html'); 64
65 // Update products 66 const newProducts = doc.querySelector('[data-collection-container]'); 67 if (newProducts) { 68 this.container.innerHTML = newProducts.innerHTML; 69 } 70
71 // Update URL without reload 72 window.history.pushState({}, '', url); 73
74 // Update active filters display 75 this.updateActiveFilters(); 76
77 // Update product count 78 this.updateProductCount(doc); 79
80 } catch (error) { 81 console.error('Filter error:', error); 82 } finally { 83 this.isLoading = false; 84 this.hideLoading(); 85 } 86 } 87
88 buildFilterUrl() { 89 const url = new URL(this.currentUrl); 90 const formData = new FormData(this.filtersForm); 91
92 // Clear existing params 93 url.search = ''; 94
95 // Add tags 96 const tags = []; 97 formData.getAll('tag').forEach(tag => { 98 if (tag) tags.push(tag); 99 }); 100
101 if (tags.length > 0) { 102 url.searchParams.set('filter.v.tag', tags.join('+')); 103 } 104
105 // Add price range 106 const priceMin = formData.get('price_min'); 107 const priceMax = formData.get('price_max'); 108
109 if (priceMin) { 110 url.searchParams.set('filter.v.price.gte', parseInt(priceMin) * 100); 111 } 112
113 if (priceMax) { 114 url.searchParams.set('filter.v.price.lte', parseInt(priceMax) * 100); 115 } 116
117 // Add variant options (size, color, etc.) 118 const options = formData.getAll('option'); 119 if (options.length > 0) { 120 url.searchParams.set('filter.v.option', options.join('+')); 121 } 122
123 // Add sort 124 const sort = document.querySelector('[data-sort-select]')?.value; 125 if (sort) { 126 url.searchParams.set('sort_by', sort); 127 } 128
129 return url.toString(); 130 } 131
132 updateActiveFilters() { 133 const activeContainer = document.querySelector('[data-active-filters]'); 134 if (!activeContainer) return; 135
136 const formData = new FormData(this.filtersForm); 137 const activeFilters = []; 138
139 // Collect active filters 140 formData.getAll('tag').forEach(tag => { 141 if (tag) { 142 const label = this.filtersForm.querySelector(`[value="${tag}"]`)?.dataset.label || tag; 143 activeFilters.push({ type: 'tag', value: tag, label }); 144 } 145 }); 146
147 const priceMin = formData.get('price_min'); 148 const priceMax = formData.get('price_max'); 149
150 if (priceMin || priceMax) { 151 const label = `Price: $${priceMin || '0'} - $${priceMax || '∞'}`; 152 activeFilters.push({ type: 'price', value: 'price', label }); 153 } 154
155 // Render active filters 156 if (activeFilters.length > 0) { 157 activeContainer.innerHTML = ` 158 <div class="active-filters"> 159 <span class="active-filters-label">Active Filters:</span> 160 ${activeFilters.map(filter => ` 161 <button 162 type="button" 163 class="active-filter-tag" 164 data-remove-filter="${filter.type}" 165 data-filter-value="${filter.value}"> 166 ${filter.label} 167 <span>×</span> 168 </button> 169 `).join('')} 170 <button type="button" class="clear-all-filters" data-clear-filters> 171 Clear All 172 </button> 173 </div> 174 `; 175
176 // Bind remove events 177 activeContainer.querySelectorAll('[data-remove-filter]').forEach(btn => { 178 btn.addEventListener('click', (e) => { 179 this.removeFilter(e.target.dataset.removeFilter, e.target.dataset.filterValue); 180 }); 181 }); 182 } else { 183 activeContainer.innerHTML = ''; 184 } 185 } 186
187 removeFilter(type, value) { 188 if (type === 'tag') { 189 const checkbox = this.filtersForm.querySelector(`[value="${value}"]`); 190 if (checkbox) { 191 checkbox.checked = false; 192 } 193 } else if (type === 'price') { 194 this.filtersForm.querySelector('[name="price_min"]').value = ''; 195 this.filtersForm.querySelector('[name="price_max"]').value = ''; 196 } 197
198 this.applyFilters(); 199 } 200
201 clearFilters() { 202 // Reset all inputs 203 this.filtersForm.reset(); 204
205 // Clear URL params 206 window.history.pushState({}, '', this.currentUrl.pathname); 207
208 // Reload products 209 this.applyFilters(); 210 } 211
212 updateProductCount(doc) { 213 const countEl = document.querySelector('[data-product-count]'); 214 if (!countEl) return; 215
216 const newCount = doc.querySelector('[data-product-count]'); 217 if (newCount) { 218 countEl.textContent = newCount.textContent; 219 } 220 } 221
222 showLoading() { 223 if (this.loadingEl) { 224 this.loadingEl.style.display = 'block'; 225 } 226
227 this.container.style.opacity = '0.5'; 228 this.container.style.pointerEvents = 'none'; 229 } 230
231 hideLoading() { 232 if (this.loadingEl) { 233 this.loadingEl.style.display = 'none'; 234 } 235
236 this.container.style.opacity = '1'; 237 this.container.style.pointerEvents = 'auto'; 238 } 239
240 debounce(func, wait) { 241 clearTimeout(this.debounceTimer); 242 this.debounceTimer = setTimeout(func, wait); 243 } 244 } 245
246 // Initialize 247 document.addEventListener('DOMContentLoaded', () => { 248 new CollectionFilters(); 249 });
Shopify AJAX Collection Filters
Implement dynamic product filtering on collection pages with real-time updates, allowing customers to filter by tags, price ranges, and product options without page reloads.
// Collection Filters Class
class CollectionFilters {
constructor(options = {}) {
this.container = document.querySelector(options.container || '[data-collection-container]');
this.filtersForm = document.querySelector(options.filtersForm || '[data-filters-form]');
this.loadingEl = document.querySelector(options.loading || '[data-loading]');
if (!this.container || !this.filtersForm) {
console.error('Required elements not found');
return;
}
this.isLoading = false;
this.currentUrl = new URL(window.location.href);
this.init();
}
init() {
// Handle filter changes
this.filtersForm.addEventListener('change', (e) => {
if (e.target.matches('[data-filter-input]')) {
this.applyFilters();
}
});
// Handle price range
const priceInputs = this.filtersForm.querySelectorAll('[data-price-input]');
priceInputs.forEach(input => {
input.addEventListener('input', () => this.debounce(() => this.applyFilters(), 500));
});
// Handle sort change
const sortSelect = document.querySelector('[data-sort-select]');
if (sortSelect) {
sortSelect.addEventListener('change', () => this.applyFilters());
}
// Clear filters
const clearBtn = document.querySelector('[data-clear-filters]');
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearFilters());
}
// Initial active filters display
this.updateActiveFilters();
}
async applyFilters() {
if (this.isLoading) return;
this.isLoading = true;
this.showLoading();
// Build filter URL
const url = this.buildFilterUrl();
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update products
const newProducts = doc.querySelector('[data-collection-container]');
if (newProducts) {
this.container.innerHTML = newProducts.innerHTML;
}
// Update URL without reload
window.history.pushState({}, '', url);
// Update active filters display
this.updateActiveFilters();
// Update product count
this.updateProductCount(doc);
} catch (error) {
console.error('Filter error:', error);
} finally {
this.isLoading = false;
this.hideLoading();
}
}
buildFilterUrl() {
const url = new URL(this.currentUrl);
const formData = new FormData(this.filtersForm);
// Clear existing params
url.search = '';
// Add tags
const tags = [];
formData.getAll('tag').forEach(tag => {
if (tag) tags.push(tag);
});
if (tags.length > 0) {
url.searchParams.set('filter.v.tag', tags.join('+'));
}
// Add price range
const priceMin = formData.get('price_min');
const priceMax = formData.get('price_max');
if (priceMin) {
url.searchParams.set('filter.v.price.gte', parseInt(priceMin) * 100);
}
if (priceMax) {
url.searchParams.set('filter.v.price.lte', parseInt(priceMax) * 100);
}
// Add variant options (size, color, etc.)
const options = formData.getAll('option');
if (options.length > 0) {
url.searchParams.set('filter.v.option', options.join('+'));
}
// Add sort
const sort = document.querySelector('[data-sort-select]')?.value;
if (sort) {
url.searchParams.set('sort_by', sort);
}
return url.toString();
}
updateActiveFilters() {
const activeContainer = document.querySelector('[data-active-filters]');
if (!activeContainer) return;
const formData = new FormData(this.filtersForm);
const activeFilters = [];
// Collect active filters
formData.getAll('tag').forEach(tag => {
if (tag) {
const label = this.filtersForm.querySelector(`[value="${tag}"]`)?.dataset.label || tag;
activeFilters.push({ type: 'tag', value: tag, label });
}
});
const priceMin = formData.get('price_min');
const priceMax = formData.get('price_max');
if (priceMin || priceMax) {
const label = `Price: $${priceMin || '0'} - $${priceMax || '∞'}`;
activeFilters.push({ type: 'price', value: 'price', label });
}
// Render active filters
if (activeFilters.length > 0) {
activeContainer.innerHTML = `
<div class="active-filters">
<span class="active-filters-label">Active Filters:</span>
${activeFilters.map(filter => `
<button
type="button"
class="active-filter-tag"
data-remove-filter="${filter.type}"
data-filter-value="${filter.value}">
${filter.label}
<span>×</span>
</button>
`).join('')}
<button type="button" class="clear-all-filters" data-clear-filters>
Clear All
</button>
</div>
`;
// Bind remove events
activeContainer.querySelectorAll('[data-remove-filter]').forEach(btn => {
btn.addEventListener('click', (e) => {
this.removeFilter(e.target.dataset.removeFilter, e.target.dataset.filterValue);
});
});
} else {
activeContainer.innerHTML = '';
}
}
removeFilter(type, value) {
if (type === 'tag') {
const checkbox = this.filtersForm.querySelector(`[value="${value}"]`);
if (checkbox) {
checkbox.checked = false;
}
} else if (type === 'price') {
this.filtersForm.querySelector('[name="price_min"]').value = '';
this.filtersForm.querySelector('[name="price_max"]').value = '';
}
this.applyFilters();
}
clearFilters() {
// Reset all inputs
this.filtersForm.reset();
// Clear URL params
window.history.pushState({}, '', this.currentUrl.pathname);
// Reload products
this.applyFilters();
}
updateProductCount(doc) {
const countEl = document.querySelector('[data-product-count]');
if (!countEl) return;
const newCount = doc.querySelector('[data-product-count]');
if (newCount) {
countEl.textContent = newCount.textContent;
}
}
showLoading() {
if (this.loadingEl) {
this.loadingEl.style.display = 'block';
}
this.container.style.opacity = '0.5';
this.container.style.pointerEvents = 'none';
}
hideLoading() {
if (this.loadingEl) {
this.loadingEl.style.display = 'none';
}
this.container.style.opacity = '1';
this.container.style.pointerEvents = 'auto';
}
debounce(func, wait) {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(func, wait);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new CollectionFilters();
});
Liquid Template - Filters Sidebar
<!-- Filters Form -->
<form class="collection-filters" data-filters-form>
<!-- Tags Filter -->
<div class="filter-group">
<h3 class="filter-title">Category</h3>
{% for tag in collection.all_tags %}
<label class="filter-option">
<input
type="checkbox"
name="tag"
value="{{ tag | handle }}"
data-label="{{ tag }}"
data-filter-input
{% if current_tags contains tag %}checked{% endif %}>
<span>{{ tag }}</span>
<span class="filter-count">({{ collection.all_products_count }})</span>
</label>
{% endfor %}
</div>
<!-- Price Filter -->
<div class="filter-group">
<h3 class="filter-title">Price Range</h3>
<div class="price-range">
<input
type="number"
name="price_min"
placeholder="Min"
min="0"
data-price-input
class="price-input">
<span>-</span>
<input
type="number"
name="price_max"
placeholder="Max"
min="0"
data-price-input
class="price-input">
</div>
</div>
<!-- Color Filter (for products with Color option) -->
<div class="filter-group">
<h3 class="filter-title">Color</h3>
<div class="color-swatches">
{% assign color_values = '' %}
{% for product in collection.products %}
{% for option in product.options_with_values %}
{% if option.name == 'Color' %}
{% for value in option.values %}
{% unless color_values contains value %}
{% assign color_values = color_values | append: value | append: ',' %}
<label class="color-swatch">
<input
type="checkbox"
name="option"
value="{{ value | handle }}"
data-label="{{ value }}"
data-filter-input>
<span class="color-swatch-inner" style="background: {{ value | downcase }}">
{{ value }}
</span>
</label>
{% endunless %}
{% endfor %}
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
</form>
<!-- Sort Dropdown -->
<select data-sort-select class="sort-select">
<option value="manual">Featured</option>
<option value="best-selling">Best Selling</option>
<option value="title-ascending">A-Z</option>
<option value="title-descending">Z-A</option>
<option value="price-ascending">Price: Low to High</option>
<option value="price-descending">Price: High to Low</option>
<option value="created-descending">Newest</option>
</select>
<!-- Active Filters -->
<div data-active-filters></div>
<!-- Product Count -->
<p><span data-product-count>{{ collection.products_count }}</span> Products</p>
<!-- Products Grid -->
<div class="products-grid" data-collection-container>
{% for product in collection.products %}
<div class="product-card">
<!-- Product card content -->
</div>
{% endfor %}
</div>
<!-- Loading Indicator -->
<div class="filters-loading" data-loading style="display: none;">
<div class="spinner"></div>
</div>
CSS Styling
.collection-filters {
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.filter-group {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
}
.filter-group:last-child {
border-bottom: none;
}
.filter-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
}
.filter-option {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
cursor: pointer;
}
.filter-option input[type="checkbox"] {
cursor: pointer;
}
.filter-count {
margin-left: auto;
color: #666;
font-size: 14px;
}
/* Price Range */
.price-range {
display: flex;
align-items: center;
gap: 10px;
}
.price-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Color Swatches */
.color-swatches {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.color-swatch input {
display: none;
}
.color-swatch-inner {
display: block;
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: border-color 0.2s;
text-indent: -9999px;
}
.color-swatch input:checked + .color-swatch-inner {
border-color: #000;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #000;
}
/* Active Filters */
.active-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
padding: 15px;
background: #f0f0f0;
border-radius: 6px;
margin-bottom: 20px;
}
.active-filters-label {
font-weight: 600;
margin-right: 5px;
}
.active-filter-tag {
padding: 5px 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
}
.clear-all-filters {
padding: 5px 15px;
background: #000;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
/* Loading */
.filters-loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
}
Features
- Real-Time Filtering: Instant results without page reload
- Multiple Filter Types: Tags, price, variants, custom options
- Active Filters Display: Shows selected filters with remove option
- Sort Integration: Combine filtering with sorting
- URL Updates: Updates URL for sharing and back button support
- Product Count: Shows number of matching products
- Responsive: Mobile-friendly design
- Debounced Inputs: Optimized for price range inputs
Related Snippets
Shopify Infinite Scroll for Collections
Add infinite scroll pagination to Shopify collection pages
JAVASCRIPTshopifyintermediate
javascriptPreview
// Infinite Scroll Class
class InfiniteScroll {
constructor(options = {}) {
this.container = options.container || '[data-products-container]';
...#shopify#infinite-scroll#pagination+2
11/18/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