Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="bsac-wrap position-relative">
    <input type="text" class="form-control form-control-lg" id="bsacInput" placeholder="Search fruits and vegetables..." autocomplete="off">
    <div class="list-group position-absolute w-100 shadow-sm d-none" id="bsacList"></div>
  </div>
</div>

Bootstrap Search Autocomplete Suggestions — Free Snippet

Bootstrap Search Autocomplete Suggestions · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Real Bootstrap list-group-item-action rows for each suggestion, styled consistently with the rest of Bootstrap
Case-insensitive substring filtering against a hardcoded JS array on every keystroke
Matched text wrapped in <mark> tags for visual highlighting
Regex-special characters in the query are escaped before building the highlight pattern
Full keyboard navigation: ArrowUp/ArrowDown to move selection, Enter to select, Escape to close
Explicit "No matches found" row instead of a silently empty dropdown
Active row auto-scrolls into view within the scrollable list on keyboard navigation
Dropdown closes automatically on outside click, matching native browser autocomplete behavior

About this UI Snippet

Bootstrap Search Autocomplete Suggestions — HTML, CSS & JavaScript

Screenshot of the Bootstrap Search Autocomplete Suggestions snippet rendered live

Autocomplete only feels right when it behaves like a native browser dropdown, so this snippet builds a keystroke-filtered suggestion list on top of a plain Bootstrap form-control and a list-group positioned underneath it with position: absolute, top: 100%, and a z-index high enough to float above surrounding content. On every input event, the current query is matched case-insensitively against a hardcoded ITEMS array with .filter(item => item.toLowerCase().includes(...)), and each surviving match is rendered as a real Bootstrap list-group-item-action button rather than a plain <li>, so it gets Bootstrap's own hover and focus styling for free.

Matching substrings are wrapped in <mark> by the highlight() function, which builds a case-insensitive RegExp from the query — after first passing it through escapeRegExp() so a query containing regex-special characters like . or ( cannot throw or produce a broken pattern — and replaces matches with an HTML <mark>$1</mark> wrapper before the string is assigned via innerHTML. That escaping step is the non-obvious edge case this snippet specifically guards against: naively feeding raw user input into new RegExp() is a common source of runtime errors or unexpected matches the moment someone types a character like * or +.

Keyboard navigation is handled entirely in one keydown listener on the input: ArrowDown and ArrowUp move an activeIndex up or down (clamped with Math.min/Math.max so it never runs past either end of the list) and setActive() applies an active-suggestion class to the corresponding row while calling scrollIntoView({ block: 'nearest' }) so keyboard-selected rows scroll into view inside the scrollable dropdown without jumping the whole page. Enter commits whichever row is currently active by calling selectItem(), and Escape or a click anywhere outside .bsac-wrap (detected with e.target.closest('.bsac-wrap') on a document-level click listener) closes the dropdown, mirroring how native OS-level autocomplete dismisses itself.

When no items match, the list still opens but shows a single muted "No matches found" row instead of staying empty or hidden, so the absence of results is communicated explicitly rather than looking like the search silently did nothing.

Rows are built as real <button type="button"> elements rather than plain <div>s specifically so they remain focusable and clickable through native Bootstrap list-group-item-action hover/focus styling, and each is wired with its own click listener calling selectItem(item), keeping mouse selection and keyboard selection funneled through the exact same function so the two input methods can never disagree about what "selecting" an item actually does.

Swapping the hardcoded ITEMS array for a real API call only changes where the filtered list comes from — debounce the input listener with setTimeout/clearTimeout before firing a fetch, then render whatever the response returns through the exact same highlight() and keyboard-navigation code, since none of that logic cares whether the array was hardcoded or fetched.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add debounced fetching from a real search API in place of the hardcoded array, or to add a small icon per suggestion row (like a category badge) alongside the highlighted match text.

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 Bootstrap 5.3 search autocomplete input using the real Bootstrap CDN (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble Bootstrap.

Requirements:
- A Bootstrap form-control input with a list-group dropdown absolutely positioned directly beneath it.
- On every keystroke, filter a hardcoded JavaScript array of strings case-insensitively by substring match and render each match as a list-group-item-action row.
- Wrap the matched substring in each result in a <mark> tag, safely escaping any regex-special characters in the typed query before building the highlight pattern.
- Support full keyboard navigation on the input: ArrowDown/ArrowUp move a highlighted active row (clamped at both ends), Enter selects the active row and fills the input, and Escape closes the dropdown.
- Show an explicit "No matches found" row when the query matches nothing, instead of leaving the dropdown empty or hidden.
- Close the dropdown automatically when the user clicks anywhere outside the input and list.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetA single search input is shown with no dropdown visible yet.
  2. 2
    Type "ap"A Bootstrap list-group dropdown opens beneath the input showing Apple, Apricot, and Grape, with the matching "ap" letters highlighted in yellow.
  3. 3
    Press the Down arrow keyThe first suggestion row gets a highlighted background indicating it is now the active keyboard selection.
  4. 4
    Keep pressing Down and UpThe active row moves through the list and never wraps past the first or last suggestion.
  5. 5
    Press EnterThe active suggestion fills the input and the dropdown closes.
  6. 6
    Type "zzz"The dropdown opens showing a muted "No matches found" message instead of staying empty.
  7. 7
    Click outside the inputThe dropdown closes immediately, the same way a native browser autocomplete list dismisses itself.

Real-world uses

Common Use Cases

SEARCH
Site search boxes and command palettes
The exact filtering and keyboard-navigation pattern used by most in-page search bars and quick-open menus.
Tag, city, or country pickers in forms
Swap the hardcoded ITEMS array for any dataset and reuse the same highlight-and-navigate behavior.
Navigation search overlays
Pair with something like Breadcrumb Overflow Dropdown to help users jump directly to a page instead of clicking through breadcrumbs.
Learning keyboard-accessible dropdowns
A clear reference for building the same kind of keyboard-driven list navigation used in Pagination with Page Jump.
CART
Product search in a storefront
Use the same highlighted-match dropdown to help shoppers find items quickly before adding them to a cart, alongside a User Menu Avatar Dropdown for account actions.

Got questions?

Frequently Asked Questions

Yes. Keep the query and activeIndex in useState (React), refs (Vue), or component properties (Angular), derive the filtered matches array on every keystroke inside the input handler, and render the list-group rows from that array with the framework's own list rendering instead of manually building DOM nodes — the highlight and keyboard-navigation logic stays the same, just triggered from framework event bindings.

It is a hardcoded ITEMS array in the JavaScript for this demo — replace the filter step with a debounced fetch call to a real search API and keep the same rendering, highlighting, and keyboard-navigation logic downstream of it.

Only after escaping it — the escapeRegExp() function escapes regex-special characters like . * + ( ) before the query is used to build a RegExp, preventing both thrown errors and unintended pattern matches from special characters a user might type.

The keydown handler checks the current activeIndex maintained by the arrow-key handlers and looks up the corresponding item in the matches array, so Enter always selects whatever row currently has the active-suggestion highlight, or does nothing if no row is active yet.

Yes — swap the list-group and list-group-item-action classes for a Tailwind dropdown pattern like an absolutely positioned div with divide-y rows; the filtering, highlighting, and keyboard-navigation JavaScript needs no changes since it targets the rows by structure, not by Bootstrap-specific classes.

The input event handler sets matches to an empty array and render() immediately adds d-none back to the dropdown, hiding it entirely rather than showing an empty list-group.