skills/storefront-ui/search-autocomplete/SKILL.md
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
npx skillsauth add finsilabs/awesome-ecommerce-skills search-autocompleteInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implement a typeahead search experience that surfaces product suggestions, categories, and content as shoppers type. Combines client-side debouncing with server-side fuzzy matching, applies merchandising rules (boosts, pins, synonyms), and renders a structured dropdown that drives measurable conversion lift.
| Platform | Recommended Approach | Why | |----------|---------------------|-----| | Shopify | Install Search & Discovery app (free, by Shopify) for synonym/boost configuration + Searchie or Boost Commerce app for full autocomplete dropdown | Search & Discovery improves the built-in Shopify search with synonyms and product boosts; Boost Commerce ($19/mo) adds a fully styled autocomplete dropdown with category results and merchandising rules | | WooCommerce | Install FiboSearch – AJAX Search for WooCommerce (free/paid) or SearchWP + SearchWP Live Search extension | FiboSearch adds an instant AJAX autocomplete dropdown to the WooCommerce search bar with product images, prices, and category results — no custom code needed | | BigCommerce | Enable Search Suggestions in Storefront → Search settings + install Klevu Smart Search or Searchspring for advanced autocomplete | BigCommerce's native search has basic autocomplete; Klevu ($449+/mo) and Searchspring add AI-powered autocomplete, synonym management, and merchandising rules | | Custom / Headless | Build with Algolia InstantSearch.js (recommended) or self-hosted Typesense; implement debounced input, AbortController for race conditions, and ARIA combobox pattern | Algolia offers the best developer experience with a generous free tier (10K searches/month); Typesense is the self-hosted alternative |
Search & Discovery app (required baseline — free):
Boost Commerce app (full autocomplete dropdown):
FiboSearch (recommended — free tier available):
SearchWP + Live Search extension:
Klevu Smart Search (advanced autocomplete):
Debounced input hook with AbortController (prevents race conditions):
// useSearchAutocomplete.js
import { useState, useEffect, useRef, useCallback } from 'react';
function debounce(fn, delay) {
let timer;
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
}
export function useSearchAutocomplete(minChars = 2) {
const [query, setQuery] = useState('');
const [results, setResults] = useState({ products: [], categories: [], suggestions: [] });
const [loading, setLoading] = useState(false);
const abortRef = useRef(null);
const fetchSuggestions = useCallback(
debounce(async (q) => {
if (q.length < minChars) { setResults({ products: [], categories: [], suggestions: [] }); return; }
if (abortRef.current) abortRef.current.abort();
abortRef.current = new AbortController();
setLoading(true);
try {
const res = await fetch(`/api/search/autocomplete?q=${encodeURIComponent(q)}&limit=5`,
{ signal: abortRef.current.signal });
setResults(await res.json());
} catch (err) { if (err.name !== 'AbortError') console.error(err); }
finally { setLoading(false); }
}, 250),
[minChars]
);
useEffect(() => { fetchSuggestions(query); }, [query, fetchSuggestions]);
return { query, setQuery, results, loading };
}
Accessible combobox dropdown (ARIA combobox + listbox pattern):
// SearchAutocomplete.jsx
import DOMPurify from 'dompurify'; // sanitize server-provided highlight HTML
export function SearchAutocomplete() {
const { query, setQuery, results, loading } = useSearchAutocomplete();
const [activeIndex, setActiveIndex] = useState(-1);
const inputRef = useRef(null);
const allItems = [...results.categories, ...results.products];
const isOpen = query.length >= 2 && allItems.length > 0;
function handleKeyDown(e) {
if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex(i => Math.min(i + 1, allItems.length - 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex(i => Math.max(i - 1, -1)); }
if (e.key === 'Enter' && activeIndex >= 0) window.location.href = allItems[activeIndex].url;
if (e.key === 'Escape') { inputRef.current.blur(); setActiveIndex(-1); }
}
return (
<div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" aria-owns="autocomplete-list">
<input ref={inputRef} type="search" value={query}
onChange={e => { setQuery(e.target.value); setActiveIndex(-1); }}
onKeyDown={handleKeyDown}
aria-autocomplete="list" aria-controls="autocomplete-list"
aria-activedescendant={activeIndex >= 0 ? `item-${activeIndex}` : undefined}
placeholder="Search products..." />
{loading && <span aria-live="polite" className="sr-only">Loading suggestions</span>}
{isOpen && (
<ul id="autocomplete-list" role="listbox" className="autocomplete-dropdown">
{results.categories.map((cat, i) => (
<li key={cat.url} id={`item-${i}`} role="option" aria-selected={activeIndex === i}>
<a href={cat.url}>Category: {cat.name} ({cat.product_count})</a>
</li>
))}
{results.products.map((product, i) => {
const idx = i + results.categories.length;
const highlighted = DOMPurify.sanitize(product._highlightResult?.name?.value ?? product.name);
return (
<li key={product.objectID} id={`item-${idx}`} role="option" aria-selected={activeIndex === idx}>
<a href={product.url} className="product-suggestion">
<img src={product.image} alt="" width="40" height="40" />
<span dangerouslySetInnerHTML={{ __html: highlighted }} />
<span>${product.price}</span>
</a>
</li>
);
})}
<li><a href={`/search?q=${encodeURIComponent(query)}`}>View all results for "{query}"</a></li>
</ul>
)}
</div>
);
}
Algolia index configuration (typo tolerance + synonyms + merchandising):
await searchClient.setSettings({
indexName: 'products',
indexSettings: {
searchableAttributes: ['name', 'brand', 'category', 'description'],
customRanking: ['desc(popularity_score)', 'desc(conversion_rate)'],
typoTolerance: 'min',
minWordSizefor1Typo: 5,
synonyms: [
{ objectID: 'shoes', type: 'synonym', synonyms: ['shoes', 'sneakers', 'footwear', 'trainers'] },
],
optionalFilters: ['is_featured:true<score=2>', 'in_stock:true<score=1>'],
},
});
AbortController to avoid race conditions when the user types quickly<mark> so shoppers see why a result appeared; sanitize server-supplied HTML before rendering| Problem | Solution |
|---------|----------|
| Stale results when user types fast | Use AbortController to cancel the previous request before issuing a new one |
| Dropdown appears behind sticky header | Set z-index explicitly on the dropdown; use a portal if inside an overflow:hidden ancestor |
| Keyboard navigation focus lost on re-render | Track activeIndex in component state, not DOM focus; re-apply aria-activedescendant on each render |
| Fuzzy matching returns irrelevant results | Configure minWordSizefor1Typo: 5 in Algolia or prefix_length: 2 in Elasticsearch to require a solid stem before fuzzy kicks in |
| Merchandising rules not applying | Rules trigger when the query matches the condition pattern — use anchoring: 'contains' not is for partial matches |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion
data-ai
Show shoppers the products they recently browsed using browser storage so they can easily pick up where they left off on your store