Accessible Autocomplete & Typeahead for Bootstrap 5 - bs-typeahead
| File Size: | 83.6 KB |
|---|---|
| Views Total: | 25701 |
| Last Update: | |
| Publish Date: | |
| Official Website: | Go to website |
| License: | MIT |
bs-typeahead is a lightweight JavaScript/TypeScript autocomplete library that turns Bootstrap text inputs into accessible typeahead search fields with local data, async data, and custom rendering.
It was originally named Bootstrap Ajax Typeahead, and the current version is its upgraded successor. The jQuery plugin API is gone. If you migrate from the old $el.typeahead() API, review the upgrade guide first because this version replaces jQuery initialization with new Typeahead(el, options).
Features:
- Supports local arrays and async remote search sources.
- Uses WAI-ARIA combobox behavior for accessible keyboard interaction.
- Positions dropdown menus with Floating UI.
- Cancels stale async requests through browser abort signals.
- Highlights matching text inside result items.
- Matches menu width to the input field.
- Supports custom item rendering and empty states.
- Includes a small React 19 wrapper.
Use Cases:
- Adding a city or airport search to a travel booking form.
- Building a product quick‑search dropdown for an e‑commerce storefront.
- Creating an employee or user picker inside an admin panel.
- Autocompleting tags or categories in a content management interface.
How To Use It:
1. Install bs-typeahead with NPM.
# Install the vanilla core package npm install @bs-typeahead/core # Install the React wrapper when you need React support npm install @bs-typeahead/react
2. You need Bootstrap 5 CSS for the dropdown menu and dropdown item styles. The core package supplies its own typeahead styles.
// Load bs-typeahead styles for menu behavior and highlighted matches. import '@bs-typeahead/core/styles.css'; // Load Bootstrap 5 CSS in your app entry file or HTML layout. import 'bootstrap/dist/css/bootstrap.min.css';
3. Create a basic autocomplete input with a local array source.
<input id="country-search" class="form-control" placeholder="Search countries..." />
// Define the shape of your data items
interface Country {
code: string;
label: string;
}
const countries: Country[] = [
{ code: 'us', label: 'United States' },
{ code: 'ca', label: 'Canada' },
{ code: 'gb', label: 'United Kingdom' },
{ code: 'de', label: 'Germany' },
{ code: 'fr', label: 'France' },
{ code: 'jp', label: 'Japan' },
];
const input = document.querySelector<HTMLInputElement>('#country-search')!;
const ta = new Typeahead<Country>(input, {
source: countries, // Pass the local array directly
displayField: 'label', // Field shown in the dropdown and matched against
valueField: 'code', // Field returned when the user picks an item
minLength: 1, // Start matching after the first character
highlight: true, // Wrap matched characters in <mark>
});
// Listen for selection
ta.on('select', (e) => {
console.log('Selected code:', e.detail.value); // 'us', 'ca', etc.
console.log('Full item:', e.detail.item);
});
4. Use Async Source with Debounce for search fields that call a REST API. The AbortController signal is passed automatically. The library cancels the previous request when the user types again before the response arrives.
interface Product {
id: number;
name: string;
sku: string;
}
const productInput = document.querySelector<HTMLInputElement>('#product-search')!;
const ta = new Typeahead<Product>(productInput, {
// Async fetcher receives the current query string and an AbortSignal
source: async (query, signal) => {
const url = `/api/products?q=${encodeURIComponent(query)}&limit=10`;
const res = await fetch(url, { signal }); // Pass signal so the request cancels on re-type
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json(); // Must resolve to Product[]
},
displayField: 'name',
valueField: 'id',
minLength: 2, // Wait for 2 characters before hitting the API
debounceMs: 300, // Wait 300 ms after the user stops typing before firing the request
maxItems: 8, // Cap the dropdown at 8 suggestions
});
// Handle async errors (network failure, non-2xx responses)
ta.on('error', (e) => {
console.error('Typeahead fetch failed:', e.detail.error);
});
ta.on('select', (e) => {
const product = e.detail.item as Product;
document.querySelector<HTMLInputElement>('#selected-sku')!.value = product.sku;
});
5. Use a custom renderer when each suggestion needs secondary text, badges, or richer Bootstrap markup.
<label for="team-member-search" class="form-label">Assign Ticket</label> <input id="team-member-search" class="form-control" type="text" autocomplete="off">
type TeamMember = {
id: number;
name: string;
role: string;
status: 'online' | 'away';
};
const memberInput = document.querySelector<HTMLInputElement>('#team-member-search')!;
const teamMembers: TeamMember[] = [
{ id: 101, name: 'Mia Carter', role: 'Frontend Engineer', status: 'online' },
{ id: 102, name: 'Ethan Brooks', role: 'Product Manager', status: 'away' },
{ id: 103, name: 'Nora Hayes', role: 'Support Lead', status: 'online' },
];
const memberTypeahead = new Typeahead<TeamMember>(memberInput, {
source: teamMembers,
displayField: 'name',
valueField: 'id',
minLength: 1,
maxItems: 5,
// Render each dropdown item with custom Bootstrap-friendly markup.
renderItem: (member) => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'dropdown-item text-start';
// Add a primary label.
const name = document.createElement('span');
name.className = 'd-block fw-semibold';
name.textContent = member.name;
// Add supporting metadata.
const role = document.createElement('small');
role.className = 'text-muted';
role.textContent = `${member.role} (${member.status})`;
item.append(name, role);
return item;
},
// Show a controlled empty state for unmatched queries.
renderEmpty: (query) => {
const empty = document.createElement('div');
empty.className = 'dropdown-item text-muted';
empty.textContent = `No team member found for "${query}"`;
return empty;
},
});
memberTypeahead.on('select', (event) => {
console.log('Assign ticket to member ID:', event.detail.value);
});
6. Use the React wrapper when your Bootstrap 5 form lives inside a React app. The wrapper forwards props to the vanilla core API.
import { Typeahead } from '@bs-typeahead/react';
type Customer = {
id: number;
fullName: string;
email: string;
};
const customers: Customer[] = [
{ id: 501, fullName: 'Avery Stone', email: '[email protected]' },
{ id: 502, fullName: 'Logan Reed', email: '[email protected]' },
{ id: 503, fullName: 'Harper Lane', email: '[email protected]' },
];
export function CustomerPicker() {
return (
<Typeahead<Customer>
source={customers}
displayField="fullName"
valueField="id"
placeholder="Search customers..."
className="form-control"
onSelect={(detail) => {
// Access the selected item from React.
console.log('Selected customer:', detail.item);
}}
/>
);
}
7. Migrating From Bootstrap Ajax Typeahead:
// Old Bootstrap Ajax Typeahead pattern.
$el.typeahead({
source: [
{ id: 1, name: 'Old Account' }
]
});
// New bs-typeahead pattern.
const input = document.querySelector('#account-search');
const accountTypeahead = new Typeahead(input, {
source: [
{ id: 9001, name: 'Northwind Wholesale' }
],
displayField: 'name',
valueField: 'id',
});
8. All configuration options:
source(T[] | (query, signal) => Promise<T[]>): Sets a local array or async fetcher. This option has no default.displayField(keyof T | (item) => string): Sets the text shown in the menu and matched against. Default:'name'.valueField(keyof T): Sets the selected value sent through theselectevent. Default:'id'.minLength(number): Sets the minimum query length before lookup starts. Default:1.debounceMs(number): Sets the delay window for async sources. Default:300.maxItems(number): Sets the maximum number of rendered options. Default:10.maxHeight(number | string): Sets the menu height limit and scroll behavior. Default: none.autoSelect(boolean): Highlights the first result. Pressing Enter commits it. Default:true.highlight(boolean): Wraps matching query text in<mark>. Default:true.matchWidth(boolean): Resizes the dropdown menu to the input width. Default:true.placement(Placement): Sets the Floating UI placement for the menu. Default:'bottom-start'.renderItem((item, ctx) => HTMLElement): Replaces the default result item renderer. Default: none.renderEmpty((query) => HTMLElement | null): Renders an empty-state item for no results. Returnnullto hide the menu. Default: none.classNames(Partial<ClassNamesConfig>): Overrides the Bootstrap 5 class names for menu, item, active item, and shown state. Default: Bootstrap 5 classes.matcher((item, query, display) => boolean): Replaces the default substring matcher for local sources. Default: substring match.
9. API methods:
// Create a new typeahead instance on a native input element.
const ta = new Typeahead(input, {
source: employees,
displayField: 'name',
valueField: 'id'
});
// Subscribe to a typeahead event.
const unsubscribeSelect = ta.on('select', (event) => {
console.log(event.detail.item);
});
// Remove a previously registered event handler.
unsubscribeSelect();
// Subscribe through the standard EventTarget API.
ta.addEventListener('open', () => {
console.log('Autocomplete menu opened.');
});
// Update the current query from application code.
ta.setQuery('billing');
10. Events:
// Fires each time the query changes through setQuery.
ta.on('query', (event) => {
console.log('Current query:', event.detail.query);
});
// Fires after a lookup resolves.
ta.on('load', (event) => {
console.log('Loaded items:', event.detail.items);
console.log('Lookup query:', event.detail.query);
});
// Fires when the user commits a result.
ta.on('select', (event) => {
console.log('Selected item:', event.detail.item);
console.log('Selected value:', event.detail.value);
console.log('Selected index:', event.detail.index);
});
// Fires when the dropdown menu opens.
ta.on('open', () => {
console.log('Menu is open.');
});
// Fires when the dropdown menu closes.
ta.on('close', () => {
console.log('Menu is closed.');
});
// Fires when an async source rejects.
ta.on('error', (event) => {
console.error('Async source error:', event.detail.error);
});
Alternatives:
Changelog:
2026-05-20
- Update for Bootstrap 5.
This awesome jQuery plugin is developed by biggora. For more Advanced Usages, please check the demo page or visit the official website.











