@smart-input/typeahead
v1.0.5
Published
Typeahead autocomplete and lookup component for building smart input experiences
Maintainers
Readme
@smart-input/typeahead
A typeahead/autocomplete lookup component for the Open Input editor. Provides intelligent suggestions as users type, with customizable data sources and filtering.
Features
- 🔍 Smart Suggestions - Show contextual suggestions as users type
- ⌨️ Keyboard Navigation - Navigate suggestions with arrow keys and select with Enter
- 🎯 Customizable Triggers - Define custom trigger characters (@, #, /, etc.)
- 📊 Flexible Data Sources - Sync or async data fetching
- 🎨 Custom Rendering - Full control over suggestion appearance
- 🔄 Real-time Filtering - Client-side or server-side filtering
- ♿ Accessible - ARIA-compliant with screen reader support
🚀 Live Demo
Try the interactive examples online: https://markgregg.github.io/smart-input/
Installation
npm install @smart-input/typeahead @smart-input/core zustand
# or
pnpm add @smart-input/typeahead @smart-input/core zustand
# or
yarn add @smart-input/typeahead @smart-input/core zustandQuick Start
Basic Usage
import { SmartInput, Editor } from '@smart-input/core';
import { TypeaheadLookup } from '@smart-input/typeahead';
import '@smart-input/core/style.css';
import '@smart-input/typeahead/style.css';
const users = [
{ id: 1, name: 'John Doe', username: 'johndoe' },
{ id: 2, name: 'Jane Smith', username: 'janesmith' },
{ id: 3, name: 'Bob Wilson', username: 'bobwilson' }
];
function App() {
return (
<SmartInput>
<TypeaheadLookup
trigger="@"
items={users}
itemToString={(user) => user.name}
onSelect={(user) => ({
text: `@${user.username}`,
style: {
backgroundColor: '#e3f2fd',
color: '#1976d2',
padding: '2px 6px',
borderRadius: '4px'
}
})}
/>
<Editor placeholder="Type @ to mention someone..." />
</SmartInput>
);
}Props
TypeaheadLookup
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| trigger | string | Yes | Character that activates the lookup (e.g., '@', '#', '/') |
| items | T[] | Yes* | Array of items to search (*or use fetchItems) |
| itemToString | (item: T) => string | Yes | Convert item to display string |
| onSelect | (item: T) => StyledBlockInfo | Yes | Convert selected item to styled block |
| fetchItems | (query: string) => Promise<T[]> | No | Async function to fetch items |
| filterItems | (items: T[], query: string) => T[] | No | Custom filtering function |
| renderItem | (item: T) => ReactNode | No | Custom item renderer |
| maxItems | number | No | Maximum suggestions to show (default: 10) |
| minQueryLength | number | No | Minimum characters before showing suggestions (default: 0) |
| debounceMs | number | No | Debounce delay for async fetching (default: 300) |
| caseSensitive | boolean | No | Case-sensitive filtering (default: false) |
Examples
User Mentions (@)
<TypeaheadLookup
trigger="@"
items={users}
itemToString={(user) => user.name}
onSelect={(user) => ({
text: `@${user.username}`,
style: {
backgroundColor: '#e3f2fd',
color: '#1976d2',
fontWeight: 'bold'
}
})}
renderItem={(user) => (
<div className="user-mention">
<img src={user.avatar} alt="" />
<div>
<div>{user.name}</div>
<div className="username">@{user.username}</div>
</div>
</div>
)}
/>Hashtags (#)
const tags = ['javascript', 'react', 'typescript', 'nodejs', 'web'];
<TypeaheadLookup
trigger="#"
items={tags}
itemToString={(tag) => tag}
onSelect={(tag) => ({
text: `#${tag}`,
style: {
color: '#2196f3',
fontWeight: '600'
}
})}
/>Commands (/)
const commands = [
{ name: 'image', description: 'Insert an image', icon: '🖼️' },
{ name: 'code', description: 'Insert code block', icon: '💻' },
{ name: 'table', description: 'Insert a table', icon: '📊' }
];
<TypeaheadLookup
trigger="/"
items={commands}
itemToString={(cmd) => cmd.name}
onSelect={(cmd) => ({
text: `/${cmd.name}`,
style: {
backgroundColor: '#f5f5f5',
fontFamily: 'monospace'
}
})}
renderItem={(cmd) => (
<div className="command-item">
<span className="icon">{cmd.icon}</span>
<div>
<div className="name">{cmd.name}</div>
<div className="description">{cmd.description}</div>
</div>
</div>
)}
/>Async Data Fetching
async function fetchUsers(query: string) {
const response = await fetch(`/api/users?q=${query}`);
return response.json();
}
<TypeaheadLookup
trigger="@"
fetchItems={fetchUsers}
itemToString={(user) => user.name}
onSelect={(user) => ({
text: `@${user.username}`,
style: { color: '#1976d2' }
})}
minQueryLength={2}
debounceMs={500}
/>Custom Filtering
function fuzzyFilter(items: User[], query: string) {
const lowerQuery = query.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(lowerQuery) ||
item.email.toLowerCase().includes(lowerQuery) ||
item.username.toLowerCase().includes(lowerQuery)
);
}
<TypeaheadLookup
trigger="@"
items={users}
filterItems={fuzzyFilter}
itemToString={(user) => user.name}
onSelect={(user) => ({
text: `@${user.username}`,
style: { color: '#1976d2' }
})}
/>Multiple Typeaheads
<SmartInput>
<TypeaheadLookup
trigger="@"
items={users}
itemToString={(u) => u.name}
onSelect={(u) => ({ text: `@${u.username}`, style: { color: 'blue' } })}
/>
<TypeaheadLookup
trigger="#"
items={tags}
itemToString={(t) => t}
onSelect={(t) => ({ text: `#${t}`, style: { color: 'green' } })}
/>
<TypeaheadLookup
trigger="/"
items={commands}
itemToString={(c) => c.name}
onSelect={(c) => ({ text: `/${c.name}`, style: { color: 'orange' } })}
/>
<Editor placeholder="Type @, #, or / to trigger suggestions..." />
</SmartInput>Keyboard Navigation
Arrow Down- Navigate to next suggestionArrow Up- Navigate to previous suggestionEnter- Select highlighted suggestionEscape- Close suggestions panelTab- Close suggestions and continue typing
Styling
Import the default styles:
import '@smart-input/typeahead/style.css';Customize with CSS classes:
.typeahead-popup- Main popup container.typeahead-list- Suggestions list.typeahead-item- Individual suggestion.typeahead-item--active- Highlighted suggestion
Example custom styles:
.typeahead-popup {
max-height: 300px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.typeahead-item {
padding: 12px 16px;
transition: background-color 0.2s;
}
.typeahead-item--active {
background-color: #f0f7ff;
color: #0066cc;
}TypeScript Support
Full TypeScript definitions are included:
import type {
TypeaheadLookupProps,
StyledBlockInfo,
TypeaheadItem
} from '@smart-input/typeahead';
interface User {
id: number;
name: string;
username: string;
}
const props: TypeaheadLookupProps<User> = {
trigger: '@',
items: users,
itemToString: (user) => user.name,
onSelect: (user) => ({
text: `@${user.username}`,
style: { color: 'blue' }
})
};Accessibility
The typeahead component follows WCAG 2.1 AA guidelines:
- ✅ ARIA labels and roles for screen readers
- ✅ Keyboard-only navigation support
- ✅ Focus management
- ✅ Screen reader announcements for results
- ✅ High contrast mode support
Advanced Usage
With React Components
Combine with @smart-input/reactblocks to render React components:
import { ReactBlocksManager } from '@smart-input/reactblocks';
function App() {
const [reactBlocks, setReactBlocks] = useState([]);
return (
<SmartInput>
<TypeaheadLookup
trigger="@"
items={users}
itemToString={(u) => u.name}
onSelect={(user) => {
const blockId = `user-${user.id}-${Date.now()}`;
setReactBlocks(prev => [...prev, {
blockId,
component: <UserCard user={user} />
}]);
return {
id: blockId,
text: `@${user.username}`,
style: { color: 'transparent' }
};
}}
/>
<ReactBlocksManager reactBlocks={reactBlocks} />
<Editor />
</SmartInput>
);
}Documentation
For more information, see:
- User Guide - Complete usage guide
- Component Reference - Component details
- Extension Development - Creating extensions
Requirements
- React 18.0.0 or higher
- @smart-input/core 1.0.0 or higher
- zustand 5.0.0 or higher
License
MIT © Mark Gregg
