More Forms Snippets
Autocomplete Input — Free HTML CSS JS Snippet
Autocomplete Input · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Autocomplete Input — Typeahead Suggestions, Highlighted Matches, Keyboard Navigation & Clear Button

An autocomplete (typeahead) input shows a dropdown of matching suggestions as the user types, allowing them to select from filtered options rather than typing the full value. It combines the freedom of a text input with the precision of a dropdown — one of the most common form patterns in address forms, search bars, country selectors, and command inputs like the command palette. This snippet provides a complete implementation with real-time filtering, match highlighting, full keyboard navigation (arrow keys + Enter + Escape), a clear button, and flag + country code display.
The filter logic
OnInput runs on every keystroke. It filters the COUNTRIES array for entries where name or code includes the query string (case-insensitive). The results are sliced to 8 to keep the dropdown compact. If no results match, a "No results for X" message renders instead of an empty list.
The match highlighting
The highlight() function wraps matching characters with a <mark> tag: text.replace(new RegExp(query, 'gi'), '<mark>$1</mark>'). The <mark> elements get a light indigo background and bold text, making the matching portion immediately visible within the suggestion. The query is escaped for regex safety with a replace on special characters.
Keyboard navigation
ArrowDown/ArrowUp increment/decrement activeIdx, bounded to the list length. setActive() adds .active class to the current item and calls scrollIntoView({ block: 'nearest' }) so items never scroll out of sight in long lists. Enter selects the active item. Escape closes the dropdown and returns to the input. aria-activedescendant tracks the highlighted item for screen readers.
The blur/close timing issue
When a suggestion is clicked, the input fires blur before the click event on the list item. Without handling this, the list closes before the click registers. The solution: use onmousedown on list items (fires before blur) with e.preventDefault() to prevent the input from losing focus. This allows the click to complete before any blur-triggered close logic runs.
Accessible ARIA attributes
The input wrapper has role="combobox" with aria-expanded and aria-haspopup="listbox". The suggestion list has role="listbox". Each item has role="option". The input has aria-controls pointing to the list ID and aria-autocomplete="list". These attributes ensure screen readers announce the suggestions correctly.
Performance with large datasets
For datasets with hundreds of items, the inline includes() filter is fast enough — arrays under 1000 items filter in under 1ms. For thousands of items, consider a trie or sorted binary search instead of linear scan. For truly large datasets (city names, product catalogues, user directories), replace the inline filter with a debounced API call and render suggestions from the server response.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Rather than puzzling out the blur-versus-click race condition on your own, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why list items use onmousedown with preventDefault instead of a plain onclick, and how that interacts with the setTimeout(closeList, 120) on the input's blur handler. The same assistant can help optimize it — ask whether the linear includes() filter over the COUNTRIES array would still be fast enough at ten thousand entries, or at what point it should be replaced with a debounced server-side search. It's also useful for extending the widget: ask it to add multi-select with removable chips, group suggestions by region with sub-headers, or wire the filter to a real API with request cancellation for stale responses. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "typeahead autocomplete input" in plain HTML, CSS, and JavaScript — no framework, no external library.
Requirements:
- A text input wrapped in a container with role="combobox", aria-expanded, and aria-haspopup="listbox", plus a clear (×) button that only appears once the input has a value.
- On every input event, filter a local array of objects by case-insensitive substring match against multiple fields (not just one), cap the results to a small number (e.g. 8), and show a distinct "no results for X" message when the query is non-empty but nothing matches.
- Wrap the matched substring of each suggestion's display text in a highlight element (not a color-only style) so the matching characters are visually distinguishable from the rest of the label.
- Full keyboard support: ArrowDown and ArrowUp move a bounded active-index highlight through the visible suggestions (using scrollIntoView with block: "nearest" so the active item is never scrolled out of view), Enter selects the currently active suggestion, and Escape closes the list without selecting.
- Critically, suggestion list items must use onmousedown with event.preventDefault() (not onclick) to handle selection, because the input's blur event fires before a click event would register, and without preventing the default mousedown behavior the list would close before the click could complete.
- Update aria-activedescendant on the input to reference the currently active suggestion's id for screen reader support, and close the list automatically on blur using a short setTimeout delay long enough for a pending mousedown-based selection to finish first.Step by step
How to Use
- 1Type in the input to see matching suggestionsStart typing a country name or code. Matching suggestions appear immediately with the typed characters highlighted in indigo. Use arrow keys to navigate up and down the list.
- 2Select with mouse or keyboardClick any suggestion to select it. Or use ArrowDown/Up to highlight, then Enter to select. The input fills with the selected name and the result shows below. Press Escape to close without selecting.
- 3Replace the COUNTRIES data with your own optionsUpdate the COUNTRIES array with your own data. Each object needs name (the display and search text) and any additional fields to show (flag, code, icon, subtitle). Update the list item innerHTML in the forEach to show your fields.
- 4Change the number of visible suggestionsUpdate .slice(0, 8) in the filter to show more or fewer suggestions at once. The list has max-height: 240px with overflow-y: auto so it scrolls for longer lists.
- 5Add debouncing for API-backed suggestionsFor remote data, replace the inline filter with a fetch call: clearTimeout(timer); timer = setTimeout(() => fetch("/api/search?q=" + q).then(r=>r.json()).then(renderSuggestions), 200). The 200ms debounce prevents a request on every keystroke.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with useState for query and suggestions, or "Tailwind" for a React + Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
This is the blur-before-click timing issue. When the user clicks a suggestion, the input fires a blur event before the click event on the list item, which triggers closeList() and removes the suggestion before the click registers. The fix: use onmousedown on list items (fires before blur) with e.preventDefault() to prevent the focus from leaving the input. This allows the mousedown to complete before the blur fires. Never use onclick alone on suggestion items in autocompletes.
Replace the inline filter with a debounced fetch: let timer; function onInput(q) { clearTimeout(timer); if (!q) { closeList(); return; } timer = setTimeout(async () => { const res = await fetch("/api/search?q=" + encodeURIComponent(q)); const data = await res.json(); filtered = data.results; renderItems(); openList(); }, 200); }. The 200ms debounce waits for the user to pause typing before fetching, preventing a request on every keypress.
Set input.value to the existing value on page load: input.value = existingValue; clear.style.display = existingValue ? "" : "none". To show the full selected state (flag + code), also set the result display: document.getElementById("ac-result").innerHTML = selectedCountry ? "Selected: <strong>" + selectedCountry.flag + " " + selectedCountry.name + "</strong>" : "". This restores the visual state correctly when editing an existing form record.
Click "JSX" to download. Manage query and filtered (array of suggestions) with useState. Run the filter logic in a useEffect([query]) or inline in the onChange handler. For keyboard navigation, manage activeIndex in useState. Use useRef for the input element for programmatic focus. The blur timing fix becomes: add onMouseDown={e => e.preventDefault()} to each suggestion li element to prevent the input from losing focus on suggestion click.