Build Your Own Autocomplete Search
DifficultyMedium
Estimated time: 2-3 hours
0 done
Description
# Build Your Own Autocomplete Search Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessi...
Requirements
- **API Integration**: Fetch suggestions from an API
- **Debouncing**: Reduce API calls while typing
- **Caching**: Store previous results for faster responses
- **Keyboard Navigation**: Full keyboard support (arrows, enter, escape)
- **Highlighting**: Highlight matching text in results
- **Loading States**: Show loading indicators
- **Error Handling**: Gracefully handle API failures
- **Accessibility**: Full ARIA support for screen readers
Overview
Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessibility. This challenge will guide you through building a production-ready autocomplete system.
Getting Started
## Step Zero: Set Up Your Environment
- **Create HTML**: Build a search input with results container
- **Mock API**: Set up a simple API endpoint or mock data
Implementation Guide
Build Your Own Autocomplete Search
Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessibility. This challenge will guide you through building a production-ready autocomplete system.
The Challenge - Building an Autocomplete Search
The functional requirements for your autocomplete system include:
- API Integration: Fetch suggestions from an API
- Debouncing: Reduce API calls while typing
- Caching: Store previous results for faster responses
- Keyboard Navigation: Full keyboard support (arrows, enter, escape)
- Highlighting: Highlight matching text in results
- Loading States: Show loading indicators
- Error Handling: Gracefully handle API failures
- Accessibility: Full ARIA support for screen readers
Step Zero: Set Up Your Environment
- Create HTML: Build a search input with results container
- Mock API: Set up a simple API endpoint or mock data
Step One: Create the Core Autocomplete Class
Example:
class Autocomplete {
constructor(input, options = {}) {
this.input = input;
this.options = {
minChars: 2,
debounceTime: 300,
maxResults: 10,
apiUrl: '/api/search',
cacheResults: true,
...options
};
this.cache = new Map();
this.currentResults = [];
this.selectedIndex = -1;
this.isLoading = false;
this.init();
}
init() {
this.createResultsContainer();
this.attachEvents();
}
createResultsContainer() {
this.results = document.createElement('div');
this.results.className = 'autocomplete-results';
this.results.setAttribute('role', 'listbox');
this.results.style.display = 'none';
this.input.parentNode.appendChild(this.results);
}
}
Step Two: Implement Debounced Search
Example:
attachEvents() {
let debounceTimer;
this.input.addEventListener('input', (e) => {
const query = e.target.value.trim();
clearTimeout(debounceTimer);
if (query.length < this.options.minChars) {
this.hideResults();
return;
}
debounceTimer = setTimeout(() => {
this.search(query);
}, this.options.debounceTime);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeyboard(e);
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.input.contains(e.target) && !this.results.contains(e.target)) {
this.hideResults();
}
});
}
Step Three: Implement Search with Caching
Example:
async search(query) {
// Check cache first
if (this.options.cacheResults && this.cache.has(query)) {
this.displayResults(this.cache.get(query), query);
return;
}
this.showLoading();
try {
const response = await fetch(
`${this.options.apiUrl}?q=${encodeURIComponent(query)}&limit=${this.options.maxResults}`
);
if (!response.ok) throw new Error('Search failed');
const data = await response.json();
// Cache results
if (this.options.cacheResults) {
this.cache.set(query, data);
}
this.displayResults(data, query);
} catch (error) {
this.showError(error.message);
} finally {
this.hideLoading();
}
}
showLoading() {
this.isLoading = true;
this.results.innerHTML = '<div class="autocomplete-loading">Loading...</div>';
this.results.style.display = 'block';
}
hideLoading() {
this.isLoading = false;
}
showError(message) {
this.results.innerHTML = `<div class="autocomplete-error">${message}</div>`;
this.results.style.display = 'block';
}
Step Four: Display Results with Highlighting
Example:
displayResults(data, query) {
this.currentResults = data;
this.selectedIndex = -1;
if (data.length === 0) {
this.results.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
this.results.style.display = 'block';
return;
}
this.results.innerHTML = '';
data.forEach((item, index) => {
const resultItem = document.createElement('div');
resultItem.className = 'autocomplete-item';
resultItem.setAttribute('role', 'option');
resultItem.setAttribute('id', `autocomplete-item-${index}`);
// Highlight matching text
const highlightedText = this.highlightMatch(item.text, query);
resultItem.innerHTML = highlightedText;
resultItem.addEventListener('click', () => {
this.selectResult(item);
});
resultItem.addEventListener('mouseenter', () => {
this.setSelected(index);
});
this.results.appendChild(resultItem);
});
this.results.style.display = 'block';
// Update ARIA attributes
this.input.setAttribute('aria-expanded', 'true');
this.input.setAttribute('aria-owns', 'autocomplete-results');
this.input.setAttribute('aria-activedescendant', '');
}
highlightMatch(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
Step Five: Implement Keyboard Navigation
Example:
handleKeyboard(e) {
if (!this.results.style.display === 'block') return;
const itemCount = this.currentResults.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.setSelected(
this.selectedIndex < itemCount - 1 ? this.selectedIndex + 1 : 0
);
break;
case 'ArrowUp':
e.preventDefault();
this.setSelected(
this.selectedIndex > 0 ? this.selectedIndex - 1 : itemCount - 1
);
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectResult(this.currentResults[this.selectedIndex]);
}
break;
case 'Escape':
e.preventDefault();
this.hideResults();
break;
case 'Tab':
this.hideResults();
break;
}
}
setSelected(index) {
// Remove previous selection
const items = this.results.querySelectorAll('.autocomplete-item');
items.forEach(item => item.classList.remove('selected'));
this.selectedIndex = index;
if (index >= 0 && index < items.length) {
const selectedItem = items[index];
selectedItem.classList.add('selected');
// Scroll into view
selectedItem.scrollIntoView({
block: 'nearest',
behavior: 'smooth'
});
// Update ARIA
this.input.setAttribute('aria-activedescendant', `autocomplete-item-${index}`);
}
}
selectResult(item) {
this.input.value = item.text;
this.hideResults();
// Trigger custom event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: item
}));
// Optional callback
if (this.options.onSelect) {
this.options.onSelect(item);
}
}
hideResults() {
this.results.style.display = 'none';
this.currentResults = [];
this.selectedIndex = -1;
this.input.setAttribute('aria-expanded', 'false');
this.input.removeAttribute('aria-activedescendant');
}
Step Six: Add Advanced Caching Strategy
Example:
class LRUCache {
constructor(maxSize = 50) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
// Remove oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
}
Step Seven: Support Different Result Formats
Example:
displayResults(data, query) {
// Support grouped results
if (data.groups) {
data.groups.forEach(group => {
const groupHeader = document.createElement('div');
groupHeader.className = 'autocomplete-group-header';
groupHeader.textContent = group.title;
this.results.appendChild(groupHeader);
group.items.forEach(item => {
this.renderResultItem(item, query);
});
});
} else {
data.forEach(item => this.renderResultItem(item, query));
}
}
renderResultItem(item, query) {
const resultItem = document.createElement('div');
resultItem.className = 'autocomplete-item';
// Support custom templates
if (this.options.renderItem) {
resultItem.innerHTML = this.options.renderItem(item, query);
} else {
resultItem.innerHTML = `
<div class="item-title">${this.highlightMatch(item.text, query)}</div>
${item.description ? `<div class="item-description">${item.description}</div>` : ''}
`;
}
resultItem.addEventListener('click', () => this.selectResult(item));
this.results.appendChild(resultItem);
}
Step Eight: Add Recent Searches
Example:
class AutocompleteWithHistory extends Autocomplete {
constructor(input, options) {
super(input, options);
this.storageKey = 'autocomplete_history';
this.maxHistory = 10;
}
loadHistory() {
const history = localStorage.getItem(this.storageKey);
return history ? JSON.parse(history) : [];
}
saveToHistory(item) {
let history = this.loadHistory();
// Remove duplicates
history = history.filter(h => h.text !== item.text);
// Add to beginning
history.unshift(item);
// Limit size
history = history.slice(0, this.maxHistory);
localStorage.setItem(this.storageKey, JSON.stringify(history));
}
showHistory() {
const history = this.loadHistory();
if (history.length === 0) return;
this.displayResults(history, '');
}
}
Step Nine: Build a Demo
Create a complete search interface with:
- Product search with images
- Recent searches section
- Category grouping
- Loading states
- Error handling
- Responsive design
Step Ten: Optional - Advanced Features
- Fuzzy search matching
- Voice input support
- Search suggestions/corrections ("Did you mean...")
- Analytics tracking
- Multi-select autocomplete
- Infinite scroll for results
The Final Step: Clean Up and Document
- Document all options and callbacks
- Create comprehensive examples
- Add accessibility testing notes
- Include performance optimization tips
Help Others by Sharing Your Solutions!
Great search experiences drive user engagement. Share your autocomplete to help others build better search interfaces!