Predictive Search Concepts and UI Patterns
Learn how predictive search works in Shopify, common UI patterns for search dropdowns, and best practices for search-as-you-type experiences.
Predictive search shows results as users type, helping them find what they need faster. Shopify provides a dedicated API for building these instant search experiences.
What is Predictive Search?
Predictive search (also called autocomplete or instant search):
- Shows results as the user types
- Doesn’t require form submission
- Returns products, collections, articles, and pages
- Uses Shopify’s search algorithm
The Predictive Search API
Shopify provides a JSON endpoint:
/search/suggest.json?q={query}&resources[type]=product,collection,article,page&resources[limit]=4Parameters:
q: The search queryresources[type]: Comma-separated resource typesresources[limit]: Max results per type (1-10)resources[options][unavailable_products]: hide, show, or lastresources[options][fields]: product fields to search
Section Rendering Approach
For HTML responses, use Section Rendering:
/search/suggest?q={query}§ion_id=predictive-searchThis returns the rendered HTML from a section, making it easy to display results.
Basic Predictive Search Structure
{# sections/predictive-search.liquid #}
{%- if predictive_search.performed -%} <div class="predictive-search__results"> {%- if predictive_search.resources.products.size > 0 -%} <div class="predictive-search__group"> <h3 class="predictive-search__heading">Products</h3> <ul class="predictive-search__list"> {%- for product in predictive_search.resources.products -%} <li class="predictive-search__item"> {% render 'predictive-search-product', product: product %} </li> {%- endfor -%} </ul> </div> {%- endif -%}
{%- if predictive_search.resources.collections.size > 0 -%} <div class="predictive-search__group"> <h3 class="predictive-search__heading">Collections</h3> <ul class="predictive-search__list"> {%- for collection in predictive_search.resources.collections -%} <li class="predictive-search__item"> <a href="{{ collection.url }}">{{ collection.title }}</a> </li> {%- endfor -%} </ul> </div> {%- endif -%}
{%- if predictive_search.resources.articles.size > 0 -%} <div class="predictive-search__group"> <h3 class="predictive-search__heading">Articles</h3> <ul class="predictive-search__list"> {%- for article in predictive_search.resources.articles -%} <li class="predictive-search__item"> <a href="{{ article.url }}">{{ article.title }}</a> </li> {%- endfor -%} </ul> </div> {%- endif -%} </div>{%- endif -%}UI Patterns
1. Dropdown Below Input
The most common pattern:
<div class="search-modal" data-predictive-search> <form action="{{ routes.search_url }}" method="get"> <input type="search" name="q" placeholder="Search..." autocomplete="off" data-search-input > </form>
<div class="search-modal__results" data-search-results hidden> {# Results populated dynamically #} </div></div>2. Full-Screen Modal
For mobile or prominent search:
<div class="search-overlay" id="search-overlay" hidden> <div class="search-overlay__content"> <form action="{{ routes.search_url }}" method="get" class="search-overlay__form"> <input type="search" name="q" placeholder="What are you looking for?" autocomplete="off" data-search-input > <button type="button" data-close-search aria-label="Close search"> {% render 'icon-close' %} </button> </form>
<div class="search-overlay__results" data-search-results> {# Results populated dynamically #} </div> </div></div>3. Search Drawer
Slide-in from side:
<search-drawer class="search-drawer" id="search-drawer"> <div class="search-drawer__overlay" data-close-search></div>
<div class="search-drawer__panel"> <div class="search-drawer__header"> <form action="{{ routes.search_url }}" method="get"> <input type="search" name="q" placeholder="Search..." data-search-input> </form> <button data-close-search aria-label="Close">×</button> </div>
<div class="search-drawer__body" data-search-results> {# Results #} </div> </div></search-drawer>Predictive Search Component
{# snippets/predictive-search.liquid #}
<predictive-search class="predictive-search" data-loading-text="Searching..."> <form action="{{ routes.search_url }}" method="get" role="search" class="predictive-search__form" > <label for="predictive-search-input" class="visually-hidden"> Search </label>
<input type="search" id="predictive-search-input" name="q" class="predictive-search__input" placeholder="Search products..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-controls="predictive-search-results" aria-expanded="false" aria-haspopup="listbox" >
<button type="submit" class="predictive-search__submit" aria-label="Search"> {% render 'icon-search' %} </button>
<button type="button" class="predictive-search__clear" hidden aria-label="Clear search" > {% render 'icon-close' %} </button>
<div class="predictive-search__loading" hidden> {% render 'loading-spinner' %} </div> </form>
<div id="predictive-search-results" class="predictive-search__results" role="listbox" hidden > {# Results populated via JavaScript #} </div></predictive-search>JavaScript Structure
class PredictiveSearch extends HTMLElement { constructor() { super(); this.input = this.querySelector('input[type="search"]'); this.resultsContainer = this.querySelector('[role="listbox"]'); this.clearButton = this.querySelector('.predictive-search__clear'); this.loadingIndicator = this.querySelector('.predictive-search__loading');
this.abortController = null; this.debounceTimer = null; }
connectedCallback() { this.input.addEventListener('input', this.onInput.bind(this)); this.input.addEventListener('focus', this.onFocus.bind(this)); this.clearButton?.addEventListener('click', this.onClear.bind(this));
// Close on click outside document.addEventListener('click', (e) => { if (!this.contains(e.target)) { this.close(); } });
// Keyboard navigation this.addEventListener('keydown', this.onKeydown.bind(this)); }
onInput() { const query = this.input.value.trim();
// Show/hide clear button this.clearButton.hidden = query.length === 0;
// Debounce the search clearTimeout(this.debounceTimer);
if (query.length < 2) { this.close(); return; }
this.debounceTimer = setTimeout(() => { this.search(query); }, 300); }
async search(query) { // Cancel previous request this.abortController?.abort(); this.abortController = new AbortController();
this.showLoading();
try { const response = await fetch( `/search/suggest?q=${encodeURIComponent(query)}§ion_id=predictive-search`, { signal: this.abortController.signal } );
if (!response.ok) throw new Error('Search failed');
const html = await response.text(); this.renderResults(html);
} catch (error) { if (error.name !== 'AbortError') { console.error('Search error:', error); this.showError(); } } finally { this.hideLoading(); } }
renderResults(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const results = doc.querySelector('.predictive-search__results');
if (results && results.innerHTML.trim()) { this.resultsContainer.innerHTML = results.innerHTML; this.open(); } else { this.showNoResults(); } }
open() { this.resultsContainer.hidden = false; this.input.setAttribute('aria-expanded', 'true'); }
close() { this.resultsContainer.hidden = true; this.input.setAttribute('aria-expanded', 'false'); }
onClear() { this.input.value = ''; this.input.focus(); this.close(); this.clearButton.hidden = true; }
showLoading() { this.loadingIndicator.hidden = false; }
hideLoading() { this.loadingIndicator.hidden = true; }
showNoResults() { this.resultsContainer.innerHTML = ` <div class="predictive-search__no-results"> No results found </div> `; this.open(); }
showError() { this.resultsContainer.innerHTML = ` <div class="predictive-search__error"> Something went wrong. Please try again. </div> `; this.open(); }
onKeydown(e) { // Handle arrow navigation, escape, enter }}
customElements.define('predictive-search', PredictiveSearch);Accessibility Considerations
-
ARIA attributes:
role="search"on formrole="listbox"on resultsaria-expandedon inputaria-controlslinking input to results
-
Keyboard navigation:
- Arrow keys to navigate results
- Enter to select
- Escape to close
-
Screen reader announcements:
- Announce number of results
- Announce when loading
announceResults(count) { const announcement = document.createElement('div'); announcement.setAttribute('role', 'status'); announcement.setAttribute('aria-live', 'polite'); announcement.className = 'visually-hidden'; announcement.textContent = `${count} results found`;
this.appendChild(announcement); setTimeout(() => announcement.remove(), 1000);}Practice Exercise
Build a predictive search component that:
- Shows a search input in the header
- Fetches results after 2+ characters
- Debounces input (300ms delay)
- Shows loading indicator
- Displays products and collections
- Supports keyboard navigation
Key Takeaways
- Use
/search/suggestfor predictive search - Section Rendering returns formatted HTML
- Debounce input to avoid excessive requests
- Cancel previous requests with AbortController
- Multiple UI patterns: dropdown, modal, drawer
- Accessibility is critical for search
- Handle loading, error, and empty states
- Keyboard navigation for power users
What’s Next?
The next lesson covers Rendering Predictive Results in detail with templates and styling.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...