JAVASCRIPTshopifyintermediate
Shopify Search Autocomplete
Add predictive search with product suggestions and instant results
Faisal Yaqoob
November 21, 2025
#shopify#search#autocomplete#ajax#predictive-search
Code
javascript
1 // Search Autocomplete Class 2 class SearchAutocomplete { 3 constructor(options = {}) { 4 this.searchInput = document.querySelector(options.input || '[data-search-input]'); 5 this.resultsContainer = document.querySelector(options.results || '[data-search-results]'); 6 this.minChars = options.minChars || 2; 7 this.debounceTime = options.debounceTime || 300; 8 this.limit = options.limit || 10; 9
10 this.debounceTimer = null; 11 this.currentQuery = ''; 12
13 if (!this.searchInput || !this.resultsContainer) { 14 console.error('Search elements not found'); 15 return; 16 } 17
18 this.init(); 19 } 20
21 init() { 22 // Input event 23 this.searchInput.addEventListener('input', (e) => { 24 this.handleInput(e.target.value); 25 }); 26
27 // Focus event 28 this.searchInput.addEventListener('focus', () => { 29 if (this.currentQuery.length >= this.minChars) { 30 this.show(); 31 } 32 }); 33
34 // Click outside to close 35 document.addEventListener('click', (e) => { 36 if (!e.target.closest('.search-container')) { 37 this.hide(); 38 } 39 }); 40
41 // Keyboard navigation 42 this.searchInput.addEventListener('keydown', (e) => { 43 this.handleKeyboard(e); 44 }); 45 } 46
47 handleInput(value) { 48 clearTimeout(this.debounceTimer); 49
50 if (value.length < this.minChars) { 51 this.hide(); 52 return; 53 } 54
55 this.debounceTimer = setTimeout(() => { 56 this.search(value); 57 }, this.debounceTime); 58 } 59
60 async search(query) { 61 this.currentQuery = query; 62 this.showLoading(); 63
64 try { 65 const response = await fetch(`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product,page,article&resources[limit]=${this.limit}`); 66
67 const data = await response.json(); 68 this.renderResults(data.resources.results); 69
70 } catch (error) { 71 console.error('Search error:', error); 72 this.showError(); 73 } 74 } 75
76 renderResults(results) { 77 this.resultsContainer.innerHTML = ''; 78
79 if (!results.products || results.products.length === 0) { 80 this.showNoResults(); 81 return; 82 } 83
84 const html = ` 85 <div class="search-results"> 86 ${results.products ? this.renderProducts(results.products) : ''} 87 ${results.pages ? this.renderPages(results.pages) : ''} 88 ${results.articles ? this.renderArticles(results.articles) : ''} 89
90 <div class="search-footer"> 91 <a href="/search?q=${encodeURIComponent(this.currentQuery)}" class="view-all-link"> 92 View all results → 93 </a> 94 </div> 95 </div> 96 `; 97
98 this.resultsContainer.innerHTML = html; 99 this.show(); 100 } 101
102 renderProducts(products) { 103 if (!products.length) return ''; 104
105 return ` 106 <div class="search-section"> 107 <h4 class="search-section-title">Products</h4> 108 <div class="search-items"> 109 ${products.map(product => ` 110 <a href="${product.url}" class="search-item"> 111 ${product.featured_image ? ` 112 <div class="search-item-image"> 113 <img src="${product.featured_image.url}" alt="${product.title}"> 114 </div> 115 ` : ''} 116 <div class="search-item-details"> 117 <h5 class="search-item-title">${this.highlight(product.title)}</h5> 118 <div class="search-item-price"> 119 ${this.formatPrice(product.price)} 120 </div> 121 </div> 122 </a> 123 `).join('')} 124 </div> 125 </div> 126 `; 127 } 128
129 renderPages(pages) { 130 if (!pages.length) return ''; 131
132 return ` 133 <div class="search-section"> 134 <h4 class="search-section-title">Pages</h4> 135 <div class="search-items"> 136 ${pages.map(page => ` 137 <a href="${page.url}" class="search-item search-item--text"> 138 <div class="search-item-details"> 139 <h5 class="search-item-title">${this.highlight(page.title)}</h5> 140 </div> 141 </a> 142 `).join('')} 143 </div> 144 </div> 145 `; 146 } 147
148 renderArticles(articles) { 149 if (!articles.length) return ''; 150
151 return ` 152 <div class="search-section"> 153 <h4 class="search-section-title">Articles</h4> 154 <div class="search-items"> 155 ${articles.map(article => ` 156 <a href="${article.url}" class="search-item search-item--text"> 157 <div class="search-item-details"> 158 <h5 class="search-item-title">${this.highlight(article.title)}</h5> 159 </div> 160 </a> 161 `).join('')} 162 </div> 163 </div> 164 `; 165 } 166
167 highlight(text) { 168 if (!this.currentQuery) return text; 169
170 const regex = new RegExp(`(${this.currentQuery})`, 'gi'); 171 return text.replace(regex, '<mark>$1</mark>'); 172 } 173
174 formatPrice(cents) { 175 return '$' + (cents / 100).toFixed(2); 176 } 177
178 showLoading() { 179 this.resultsContainer.innerHTML = ` 180 <div class="search-loading"> 181 <div class="spinner"></div> 182 <p>Searching...</p> 183 </div> 184 `; 185 this.show(); 186 } 187
188 showNoResults() { 189 this.resultsContainer.innerHTML = ` 190 <div class="search-no-results"> 191 <p>No results found for "${this.currentQuery}"</p> 192 <p class="search-suggestion">Try different keywords or browse our collections</p> 193 </div> 194 `; 195 this.show(); 196 } 197
198 showError() { 199 this.resultsContainer.innerHTML = ` 200 <div class="search-error"> 201 <p>Something went wrong. Please try again.</p> 202 </div> 203 `; 204 } 205
206 show() { 207 this.resultsContainer.classList.add('is-visible'); 208 } 209
210 hide() { 211 this.resultsContainer.classList.remove('is-visible'); 212 } 213
214 handleKeyboard(e) { 215 const items = this.resultsContainer.querySelectorAll('.search-item'); 216
217 if (e.key === 'ArrowDown') { 218 e.preventDefault(); 219 // Navigate down 220 } else if (e.key === 'ArrowUp') { 221 e.preventDefault(); 222 // Navigate up 223 } else if (e.key === 'Escape') { 224 this.hide(); 225 } 226 } 227 } 228
229 // Initialize 230 document.addEventListener('DOMContentLoaded', () => { 231 new SearchAutocomplete({ 232 input: '[data-search-input]', 233 results: '[data-search-results]', 234 minChars: 2, 235 limit: 8 236 }); 237 });
Shopify Search Autocomplete
Implement a predictive search feature that shows product suggestions, collections, and pages as users type in the search box.
// Search Autocomplete Class
class SearchAutocomplete {
constructor(options = {}) {
this.searchInput = document.querySelector(options.input || '[data-search-input]');
this.resultsContainer = document.querySelector(options.results || '[data-search-results]');
this.minChars = options.minChars || 2;
this.debounceTime = options.debounceTime || 300;
this.limit = options.limit || 10;
this.debounceTimer = null;
this.currentQuery = '';
if (!this.searchInput || !this.resultsContainer) {
console.error('Search elements not found');
return;
}
this.init();
}
init() {
// Input event
this.searchInput.addEventListener('input', (e) => {
this.handleInput(e.target.value);
});
// Focus event
this.searchInput.addEventListener('focus', () => {
if (this.currentQuery.length >= this.minChars) {
this.show();
}
});
// Click outside to close
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-container')) {
this.hide();
}
});
// Keyboard navigation
this.searchInput.addEventListener('keydown', (e) => {
this.handleKeyboard(e);
});
}
handleInput(value) {
clearTimeout(this.debounceTimer);
if (value.length < this.minChars) {
this.hide();
return;
}
this.debounceTimer = setTimeout(() => {
this.search(value);
}, this.debounceTime);
}
async search(query) {
this.currentQuery = query;
this.showLoading();
try {
const response = await fetch(`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product,page,article&resources[limit]=${this.limit}`);
const data = await response.json();
this.renderResults(data.resources.results);
} catch (error) {
console.error('Search error:', error);
this.showError();
}
}
renderResults(results) {
this.resultsContainer.innerHTML = '';
if (!results.products || results.products.length === 0) {
this.showNoResults();
return;
}
const html = `
<div class="search-results">
${results.products ? this.renderProducts(results.products) : ''}
${results.pages ? this.renderPages(results.pages) : ''}
${results.articles ? this.renderArticles(results.articles) : ''}
<div class="search-footer">
<a href="/search?q=${encodeURIComponent(this.currentQuery)}" class="view-all-link">
View all results →
</a>
</div>
</div>
`;
this.resultsContainer.innerHTML = html;
this.show();
}
renderProducts(products) {
if (!products.length) return '';
return `
<div class="search-section">
<h4 class="search-section-title">Products</h4>
<div class="search-items">
${products.map(product => `
<a href="${product.url}" class="search-item">
${product.featured_image ? `
<div class="search-item-image">
<img src="${product.featured_image.url}" alt="${product.title}">
</div>
` : ''}
<div class="search-item-details">
<h5 class="search-item-title">${this.highlight(product.title)}</h5>
<div class="search-item-price">
${this.formatPrice(product.price)}
</div>
</div>
</a>
`).join('')}
</div>
</div>
`;
}
renderPages(pages) {
if (!pages.length) return '';
return `
<div class="search-section">
<h4 class="search-section-title">Pages</h4>
<div class="search-items">
${pages.map(page => `
<a href="${page.url}" class="search-item search-item--text">
<div class="search-item-details">
<h5 class="search-item-title">${this.highlight(page.title)}</h5>
</div>
</a>
`).join('')}
</div>
</div>
`;
}
renderArticles(articles) {
if (!articles.length) return '';
return `
<div class="search-section">
<h4 class="search-section-title">Articles</h4>
<div class="search-items">
${articles.map(article => `
<a href="${article.url}" class="search-item search-item--text">
<div class="search-item-details">
<h5 class="search-item-title">${this.highlight(article.title)}</h5>
</div>
</a>
`).join('')}
</div>
</div>
`;
}
highlight(text) {
if (!this.currentQuery) return text;
const regex = new RegExp(`(${this.currentQuery})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
showLoading() {
this.resultsContainer.innerHTML = `
<div class="search-loading">
<div class="spinner"></div>
<p>Searching...</p>
</div>
`;
this.show();
}
showNoResults() {
this.resultsContainer.innerHTML = `
<div class="search-no-results">
<p>No results found for "${this.currentQuery}"</p>
<p class="search-suggestion">Try different keywords or browse our collections</p>
</div>
`;
this.show();
}
showError() {
this.resultsContainer.innerHTML = `
<div class="search-error">
<p>Something went wrong. Please try again.</p>
</div>
`;
}
show() {
this.resultsContainer.classList.add('is-visible');
}
hide() {
this.resultsContainer.classList.remove('is-visible');
}
handleKeyboard(e) {
const items = this.resultsContainer.querySelectorAll('.search-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
// Navigate down
} else if (e.key === 'ArrowUp') {
e.preventDefault();
// Navigate up
} else if (e.key === 'Escape') {
this.hide();
}
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new SearchAutocomplete({
input: '[data-search-input]',
results: '[data-search-results]',
minChars: 2,
limit: 8
});
});
Liquid Template
<!-- Search Container -->
<div class="search-container">
<form action="/search" method="get" class="search-form">
<input
type="text"
name="q"
placeholder="Search products..."
autocomplete="off"
data-search-input
class="search-input">
<button type="submit" class="search-submit">
<svg width="20" height="20" viewBox="0 0 20 20">
<path d="M8 0a8 8 0 015.293 14.707l5.853 5.854-1.414 1.414-5.854-5.853A8 8 0 118 0zm0 2a6 6 0 100 12A6 6 0 008 2z"/>
</svg>
</button>
</form>
<div class="search-results-container" data-search-results></div>
</div>
CSS Styling
.search-container {
position: relative;
width: 100%;
max-width: 600px;
}
.search-form {
position: relative;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
padding: 12px 50px 12px 20px;
border: 2px solid #ddd;
border-radius: 25px;
font-size: 16px;
transition: border-color 0.3s;
}
.search-input:focus {
outline: none;
border-color: #000;
}
.search-submit {
position: absolute;
right: 15px;
background: none;
border: none;
cursor: pointer;
padding: 5px;
}
/* Results Container */
.search-results-container {
position: absolute;
top: calc(100% + 10px);
left: 0;
right: 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
max-height: 500px;
overflow-y: auto;
z-index: 1000;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.search-results-container.is-visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Search Sections */
.search-section {
padding: 15px;
border-bottom: 1px solid #eee;
}
.search-section:last-child {
border-bottom: none;
}
.search-section-title {
font-size: 12px;
text-transform: uppercase;
color: #666;
margin-bottom: 10px;
font-weight: 600;
}
/* Search Items */
.search-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
border-radius: 6px;
text-decoration: none;
color: #333;
transition: background 0.2s;
}
.search-item:hover {
background: #f5f5f5;
}
.search-item-image {
flex-shrink: 0;
width: 60px;
height: 60px;
border-radius: 4px;
overflow: hidden;
}
.search-item-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.search-item-details {
flex: 1;
}
.search-item-title {
font-size: 14px;
margin-bottom: 5px;
font-weight: 500;
}
.search-item-title mark {
background: #ffeb3b;
padding: 2px 4px;
border-radius: 2px;
}
.search-item-price {
font-size: 14px;
font-weight: bold;
color: #000;
}
/* Loading State */
.search-loading {
padding: 40px;
text-align: center;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px 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); }
}
/* No Results */
.search-no-results {
padding: 40px 20px;
text-align: center;
color: #666;
}
.search-suggestion {
font-size: 14px;
margin-top: 10px;
}
/* Footer */
.search-footer {
padding: 15px;
background: #f9f9f9;
border-top: 1px solid #eee;
text-align: center;
}
.view-all-link {
color: #0066cc;
text-decoration: none;
font-weight: 500;
}
.view-all-link:hover {
text-decoration: underline;
}
Advanced: With Collections
// Include collections in search
async search(query) {
this.currentQuery = query;
this.showLoading();
try {
// Get products
const productsResponse = await fetch(
`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=5`
);
const productsData = await productsResponse.json();
// Get collections
const collectionsResponse = await fetch(
`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=collection&resources[limit]=3`
);
const collectionsData = await collectionsResponse.json();
const results = {
products: productsData.resources.results.products,
collections: collectionsData.resources.results.collections
};
this.renderResults(results);
} catch (error) {
console.error('Search error:', error);
this.showError();
}
}
Features
- Real-Time: Instant search results as you type
- Debouncing: Optimized API calls
- Multi-Resource: Search products, pages, articles, collections
- Highlighting: Query terms highlighted in results
- Keyboard Navigation: Arrow keys and ESC support
- Responsive: Mobile-friendly design
- Loading States: Visual feedback during search
- Error Handling: Graceful failure messages
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 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 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