I've always found that one of the best ways to truly understand a concept is to build it from scratch. That's why I wrote this guide. I wanted to break down the autocomplete—a component we use every day—into its fundamental parts. My hope is that by building it together, you'll not only gain a powerful new tool for your projects but also a deeper appreciation for the details that create a great user experience. Let's dive in!
Autocomplete is a feature that users have come to expect. It makes interfaces faster, smarter, and more user-friendly. While it might seem complex, building a professional-grade autocomplete component from scratch is an excellent way to sharpen your vanilla JavaScript skills.
In this tutorial, we'll build a fully-featured, accessible, and performant autocomplete component. We will cover:
- Fetching data from a live API (
jsonplaceholder.typicode.com
). - Filtering results and dynamically rendering a suggestions list.
- Implementing keyboard navigation (Up, Down, Enter, Escape).
- Adding debouncing to prevent excessive API calls.
- Ensuring accessibility with ARIA attributes.
- Styling the component with Tailwind CSS.
Finally, we'll see how the principles we've learned apply to more specialised libraries like places-autocomplete-js
for handling Google Maps address lookups.
The Final Result
Here's what we'll be building: a clean, fast, and accessible autocomplete for searching users.
The HTML Structure (autocomplete.html
)
We'll start with a simple but powerful HTML structure. The key is the <input>
element, which we'll augment with several ARIA attributes to make it accessible.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Autocomplete Example</title>
<!-- We are using Tailwind CSS for styling -->
<link href="./index.css" rel="stylesheet">
<style>
/* Custom styles for the suggestion list */
.suggestions-list {
border: 1px solid #ddd;
border-top: none;
list-style: none;
padding: 0;
margin: 0;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.suggestions-list li {
padding: 10px;
cursor: pointer;
}
.suggestions-list li.selected {
background-color: #f0f0f0;
}
.suggestions-list .no-results {
padding: 10px;
color: #888;
cursor: default;
}
</style>
</head>
<body>
<div class="p-10">
<h1 class="text-2xl font-bold mb-4">Autocomplete Example</h1>
<div class="autocomplete-container relative w-full max-w-xs">
<input
type="text"
id="autocomplete-input"
class="border-2 border-gray-300 rounded-md p-2 w-full"
placeholder="Search for a user..."
autocomplete="off"
role="combobox"
aria-autocomplete="list"
aria-haspopup="true"
aria-expanded="false"
aria-controls="suggestions-list"
/>
<ul id="suggestions-list" class="suggestions-list absolute w-full bg-white" role="listbox"></ul>
</div>
</div>
<script src="app.js" type="module"></script>
</body>
</html>
The ARIA attributes tell screen readers that this input is a combobox
that will display a list
of suggestions, whether the list is expanded
, and which element it controls
.
The JavaScript Logic (app.js
)
This is where the magic happens. Our app.js
file handles everything from fetching data to managing user interaction.
1. Fetching and Debouncing
First, we fetch the user data from the JSONPlaceholder API. To avoid overwhelming the API (and our own code) with requests as the user types, we wrap our input handler in a debounce
function. This ensures the search logic only runs after the user has paused typing for 300ms.
// Debounce function to limit the rate of execution
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
};
// The actual input handler
const handleInput = (e) => {
const query = e.target.value.toLowerCase();
if (query.length < 1) {
clearSuggestions();
return;
}
const filteredSuggestions = allUsers.filter(user =>
user.name.toLowerCase().includes(query)
);
renderSuggestions(filteredSuggestions);
};
// Attach the debounced handler
input.addEventListener('input', debounce(handleInput, 300));
2. Rendering Suggestions
The renderSuggestions
function dynamically creates the list of <li>
elements. For each suggestion, we set an id
and role="option"
for accessibility. We also update aria-expanded
to true
so screen readers know the list is now visible.
const renderSuggestions = (suggestions) => {
clearSuggestions();
input.setAttribute('aria-expanded', 'true');
suggestions.forEach((suggestion, index) => {
const li = document.createElement('li');
li.id = `suggestion-${index}`;
li.setAttribute('role', 'option');
// ... (add content and event listener)
suggestionsList.appendChild(li);
});
};
3. Keyboard Navigation and Accessibility
A professional autocomplete must be fully keyboard-navigable. We listen for ArrowDown
, ArrowUp
, Enter
, and Escape
.
When the user navigates with the arrow keys, the updateSelectedSuggestion
function highlights the active item and, crucially, updates the aria-activedescendant
attribute on the input. This tells the screen reader which option is currently focused, providing a seamless experience for all users.
const updateSelectedSuggestion = () => {
const suggestions = suggestionsList.querySelectorAll('li:not(.no-results)');
suggestions.forEach((li, index) => {
if (index === selectedSuggestionIndex) {
li.classList.add('selected');
// This is key for accessibility!
input.setAttribute('aria-activedescendant', li.id);
} else {
li.classList.remove('selected');
}
});
};
Applying These Principles in the Real World
The autocomplete component we've built is a powerful, general-purpose foundation. These features—debouncing, keyboard navigation, and accessibility—can be adapted for countless applications, from searching products to tagging articles.
As a prime example of a specialised application, consider places-autocomplete-js
This library acts as a wrapper for the Google Maps Places (New) API. It provides a convenient autocomplete gateway for the user to search, filter, and navigate the complex responses from the API and select a desired address. It takes care of the API's specific requirements (like session tokens and structured data), so the developer can focus on using the final address data.
Let's Discuss!
Thanks for reading! I hope you found this tutorial helpful. Building components like this is a journey, and there are always new things to learn.
- What are your thoughts? Would you approach this differently?
- Have any questions? Leave a comment below.
- How would you use this? I'd love to hear how you might use these techniques in your own projects.
If you found this article useful, please consider sharing it on social media or with your co-workers.
Conclusion
Building a feature-rich autocomplete from scratch is a fantastic way to master core JavaScript skills. The techniques covered here provide a solid foundation for creating interactive and user-friendly components.
Top comments (0)