As an experienced full-stack developer, combo boxes are one of those standard UI components I implement on a regular basis. Whether allowing users to select options or enter custom values, combo boxes enable an intuitive input experience.
In this comprehensive guide, we’ll build a robust, editable combo box from the ground up using JavaScript.
Combo Boxes: UX Game Changers
Before we code, let’s highlight some key UX advantages combo boxes bring:
Typeahead Finding
Combo boxes only show options matching the entered text, allowing for rapid filtering through long lists. This frees users from scanning endless menus.
Recognition over Recall
Seeing options jogs memory better than trying to recall options from scratch. Combo boxes use recognition to aid the search process.
Clear Expectations
Pre-defined options set user expectations for valid inputs while allowing the flexibility of custom entries as needed.
Space Saving
Combo boxes present a compact form factor, ideal for crowded interfaces. Options are neatly tucked away until needed.
Accessibility
Features like ARIA tags, keyboard support, and screen reader compatibility boost accessibility when implemented properly. We’ll cover these later.
Thoughtful combo boxes make it easy for users to search, select, or specify options as they prefer.
Now let’s see how to build this flexible control with JavaScript…
Markup Structure
Combo boxes require a text input coupled with a container to hold options.
<!-- Combo box container -->
<div class="combo-box">
<!-- Text input -->
<input type="text" id="combo-input">
<!-- Options list -->
<ul id="combo-list">
<li>Option 1</li>
<li>Option 2</li>
</ul>
</div>
This simple structure gives us flexibility to inject all functionality with JavaScript.
Creating the Text Input
We’ll first setup the text input that allows users to search and select options:
// Text input
const input = document.createElement("input");
input.type = "text";
input.id = "combo-input";
// Parent container
const container = document.querySelector(".combo-box");
// Add input to DOM
container.appendChild(input);
Next we’ll handle user input events:
// Input event handler
input.addEventListener("input", filterOptions);
// Called on input change
function filterOptions() {
// Get current value
let value = input.value;
// Filter logic...
}
The filterOptions() function will handle filtering the options list based on text input. We’ll implement this shortly.
First, let’s build out the options list.
Creating the Options List
The options list displays selectable choices stored in an array:
// Array of options
let options = [
"Option 1",
"Option 2",
"Option 3"
];
We can turn this array into a populated <ul> with:
// Create list container
let list = document.createElement("ul");
list.id = "combo-list";
// Loop through options
options.forEach(item => {
let li = document.createElement("li");
li.textContent = item;
list.appendChild(li);
});
// Add list to DOM
container.appendChild(list);
This gives us a basic options list to work with!
Later, we’ll handle selecting options and implement filtering.
Flexible Data Sources
Hard-coded arrays work, but in real applications combo box options often come from dynamic API responses:
async function getOptions(){
const response = await fetch("/options");
return response.json();
}
getOptions()
.then(data => {
// Set list
})
.catch(err => {
// Handle errors
});
We can handle disparate data sources by normalizing to arrays:
function normalizeOptions(options) {
// Array of arrays
if(Array.isArray(options[0])) {
return options.flat();
}
// Array of objects
if(typeof options[0] === "object") {
return options.map(option => option.name);
}
// Simple array
return options;
}
Now regardless of source, we have a unified list for our combo box.
Side note: If options rarely change, keep an approved list with the component state instead of unnecessary API calls.
Filtering Logic
As the user types into the input, we need to filter the options in real time:
function filterOptions() {
// Get current value
const query = input.value.toLowerCase();
// Filter items
const results = options.filter(option => {
// Convert to lowercase
const text = option.toLowerCase();
// Check if text contains query
return text.indexOf(query) !== -1;
});
// Display in list
displayOptions(results);
}
We filter out entries that don’t contain the input query by checking indexOf() on lowercase strings for partial matching.
The filtered subset gets passed to a displayOptions() function to update the visual list:
function displayOptions(results) {
// Clear list
while (list.firstChild) {
list.removeChild(list.firstChild);
}
// Rebuild
results.forEach(result => {
const li = document.createElement("li");
li.textContent = result;
list.append(li);
});
}
Now only matching options are shown in real-time!
Selecting Options
When the user selects an item, we need to update the input value and close the list:
list.addEventListener("click", selectOption);
function selectOption(e) {
input.value = e.target.textContent;
list.classList.remove("open");
}
This handles selection logic, while a simple class toggle opens/closes the list, which we’ll cover next.
Opening/Closing Behaviors
Combo boxes expand and collapse in response to user actions.
Opening the list when clicking the input makes discovery intuitive:
input.addEventListener("click", openList);
function openList() {
list.classList.add("open");
}
Clicking outside should close the list automatically:
document.body.addEventListener("click", event => {
// Check clicks outside combobox
if(!container.contains(event.target)) {
closeList();
}
});
function closeList() {
list.classList.remove("open");
}
This activates outside-click behavior.
Autocomplete Functionality
As the user types, we may want to auto-complete common entries to speed up input.
First we define eligible autocompletes:
// Autocomplete options
const autocompletes = [
"Option 1",
"Option 2"
];
Then check if the input matches an option:
// In filterOptions()...
function filterOptions() {
// Get input text
const query = input.value;
// Check matching autocompletes
const autoComplete = autocompletes.find(option => {
return option.startsWith(query);
});
// Apply autocomplete if found
if(autoComplete) {
input.value = autoComplete;
}
}
This handles autocompletion checks while filtering as needed.
Combo Boxes vs. Selects
Before moving further, let’s compare combo boxes to standard select dropdowns:
Combo Box Benefits
- Custom value entry
- Search/filter capability
- Easier data handling (e.g large lists)
- More flexibility and control
Select Benefits
- Native form integration
- Less code and setup
- Touch capability
If form integration or mobile use is needed, default selects may suffice.
Otherwise, custom combo boxes provide enhanced UX and developer flexibility.
Now let‘s explore some best practices…
UI & UX Considerations
Crafting quality combo box experiences requires attention to UI/UX details. Here are some key considerations:
Clear Visual Cues
Use hover states, contrasting colors, and ample spacing to indicate functionality. Employ animations for opening/closing lists to prevent jarring transitions.
Screen Reader Support
Implement ARIA combobox roles, labels, live region alerts etc. Support keyboard navigation and verbalization.
Mobile Optimization
Ensure options are easy to tap on touch devices. Use native selects or hybrid UI libraries if advanced mobile features are required.
Predictive Behaviors
Predict user intent with features like search autocompletion, recent selections, contextual recommendations etc.
Load States & Empty Views
Display skeleton screens while fetching options. Communicate clearly when no results match filters.
Accessible Markup
Follow semantic structure and validation practices for robust, accessible code.
Let‘s look at some examples…
Illustrative Combo Box Interface
Here is an example combo box design demonstrating UX best practices:

Notice the:
- Clean styling
- Search autocompletes
- Distinct selected state
- Scrollable list
- Emphasized search matches
- Defined structure
This improves usability through intentional design choices.
Sample Accessible HTML
Here is conceptual markup adhering to modern accessibility standards:
<div class="combo-box">
<label for="combo">Choose an option:</label>
<input name="combo" type="text" aria-controls="combo-list">
<ul class="option-list" id="combo-list" role="listbox" aria-label="Combo box options">
<li id="option1" role="option">Option 1</li>
<li id="option2" role="option">Option 2</li>
</ul>
</div>
Key aspects like ARIA roles, labels, semantic ids, and keyboard support boost accessibility.
Now that we‘ve covered UX considerations, let‘s look at practical examples.
Building Sample Combo Boxes
Let’s walk through some sample use cases to demonstrate real-world implementation:
Example 1: Selection Combo Box
For basic option selection without custom values, we can disable free-form input:
// Disable input edits
input.readOnly = true;
// Disable spellcheck
input.setAttribute("spellcheck", false);
We can fetch options from a server and normalize them:
// Get options
async function getOptions() {
let response = await fetch("/options");
return normalizeOptions(await response.json());
}
getOptions().then(displayOptions);
And voilà – a selection-only combo box leveraging external data!
Example 2: Object-based Combo Box
For more complex applications, options may be object-based rather than simple strings:
let options = [
{
id: 1,
name: "Option 1",
category: "A"
},
{
id: 2,
name: "Option 2",
category: "B"
}
];
We can update our functions to handle objects accordingly:
// Display object options
function displayOptions(optionList) {
list.innerHTML = "";
optionList.forEach(option => {
let li = document.createElement("li");
li.dataset.id = option.id;
li.textContent = option.name;
list.appendChild(li);
});
}
// Get ID of selected
function selectOption(e) {
let {id} = e.target.dataset;
// Use option ID
doSomething(id);
}
This provides the ability to track and select full option objects vs just display names.
Example 3: Multi-Selection Combo Box
With some modifications, we can allow multiple tag-style selections:
// Track selections
let selections = [];
// Multi select handler
function selectOption(e) {
let name = e.target.textContent;
// Toggle selection
if(selections.includes(name)) {
deselect(name);
} else {
selections.push(name);
}
// Display tags
displaySelections();
}
// Show selections
function displaySelections() {
let html = selections.map(item => {
return `<span class="tag">${item}<span>`;
}).join("");
selectionsDisplay.innerHTML = html;
}
This enables choosing multiple options before submitting the full group.
The possibilities are vast when leveraging combo boxes‘ capabilities.
Key Takeaways
Let‘s recap the core concepts we‘ve covered:
- Combo boxes provide intuitive input/selection experiences through typing and choosing.
- Text inputs coupled with filtered selectable lists form the basis.
- Dynamic search results create engaging discovery processes.
- Custom value entry increases flexibility over standard dropdowns.
- Object and external data support further extends use cases.
- Features like multiselect take things up a notch.
- Careful UI/UX design creates usable, accessible controls.
- Overall, customizable combo boxes power robust input flows.
While basic at heart, combo boxes prove integral in crafting complex, high-value search/select user journeys. I hope breaking down how to build one from scratch helps unlock their full potential.
Let me know if you have any other combo box questions!


