Autocomplete Input — Free HTML CSS JS Snippet

Autocomplete Input · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real-time filter: includes() check on name and code, sliced to 8 results
Match highlight: RegExp replace with <mark> tags, query regex-escaped
Keyboard nav: ArrowDown/Up moves activeIdx, Enter selects, Escape closes
onmousedown + e.preventDefault() prevents blur-before-click timing issue
Clear button: appears when input has value, resets state and refocuses input
aria-activedescendant tracks highlighted item for screen reader announcement
Blur timeout: setTimeout(closeList, 120) allows click events to complete first
No results state: shows message when filter returns empty array

About this UI Snippet

Autocomplete Input — Typeahead Suggestions, Highlighted Matches, Keyboard Navigation & Clear Button

Screenshot of the Autocomplete Input snippet rendered live

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:

text
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

  1. 1
    Type 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.
  2. 2
    Select 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.
  3. 3
    Replace 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.
  4. 4
    Change 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.
  5. 5
    Add 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.
  6. 6
    Export 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

Country and city selector in address and profile forms
The flag + name + code pattern is the standard for international address forms. Users can type a partial name or ISO code to find their country quickly without scrolling a 200-item dropdown.
User mention and tag autocomplete in text editors
Wire to a users API: fetch suggestions on @ keypress, show avatars and usernames in the list, insert the selected user mention into the editor content at the cursor position.
Product and inventory search with instant results
Connect to a product search API for an instant product finder. Show product image thumbnails, names, and prices in the suggestion list. Selecting a product navigates to the product page or adds it to a cart or order.
Command input and keyboard shortcut launcher
Use as a mini command palette for quick actions. The suggestion list shows command names and descriptions. Unlike the full Command Palette snippet, this inline variant works inside forms and settings panels without a modal overlay.
Study match highlighting and keyboard navigation patterns
The highlight() function shows how to wrap regex matches in HTML elements inline — the standard technique for search result highlighting. The keyboard navigation demonstrates how to manage an activeIdx state with bounded increment/decrement.
API-backed typeahead with debounced fetch requests
Replace the inline filter with a debounced fetch call to a search API. Add a loading spinner to the input during the fetch. Cache results per query string to avoid re-fetching the same query. The UI pattern is identical whether data is local or remote.

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.