@zeptonow/input-typeahead
Version:
An input typeahead component for react applications, convert any input into a typeahead with a dropdown of suggestions.
339 lines (283 loc) • 11.3 kB
Markdown

[](https://www.npmjs.com/package/@zeptonow/input-typeahead)
[](https://opensource.org/licenses/MIT)
A lightweight, customizable React typeahead component that converts any input into a powerful typeahead with a dropdown of suggestions.
- 🚀 **Lightweight and Performant**: Minimal bundle size with optimized rendering
- 🎨 **Fully Customizable**: Style every aspect of the typeahead to match your UI
- 🧩 **Composable API**: Easily integrate with your existing components
- 🌐 **Keyboard Navigation**: Full keyboard support for accessibility
- 📱 **Responsive**: Works great on all device sizes
- 🔍 **Flexible Search**: Supports custom filtering logic
- 🔄 **Async Data**: Support for loading data from remote sources
- 🔀 **Nested Options**: Support for hierarchical dropdown menus with categories
```bash
npm install @zeptonow/input-typeahead
yarn add @zeptonow/input-typeahead
pnpm add @zeptonow/input-typeahead
```
```jsx
import React, { useRef } from "react";
import { Typeahead } from "@zeptonow/input-typeahead";
function MyComponent() {
const inputRef = useRef(null);
const options = [
{
label: "Fruits",
children: [
{ label: "Apple", value: "apple", description: "Red and juicy" },
{ label: "Banana", value: "banana", description: "Yellow and sweet" },
{ label: "Cherry", value: "cherry", description: "Small and tart" },
],
},
{
label: "Vegetables",
children: [
{ label: "Carrot", value: "carrot", description: "Orange and crunchy" },
{
label: "Broccoli",
value: "broccoli",
description: "Green and healthy",
},
],
},
];
return (
<>
<input ref={inputRef} type="text" placeholder="Type / to see options" />
<Typeahead
inputRef={inputRef}
options={options}
triggerChar="/"
position="cursor"
activateMode="multiple"
onSelect={(option) => console.log("Selected:", option)}
/>
</>
);
}
```
The typeahead component supports the following keyboard shortcuts for efficient navigation:
| Key | Function |
| ------------------------------------ | ----------------------------------------------------------- |
| **Arrow Up** | Navigate to previous option |
| **Arrow Down** | Navigate to next option |
| **Enter** | Select the currently highlighted option |
| **Escape** | Close the typeahead dropdown |
| **Option** (Mac) / **Alt** (Windows) | Go back to the parent menu when navigating nested dropdowns |
The typeahead component supports hierarchical options with nested categories. When using nested options:
1. Navigate into a category by selecting it with Enter or clicking on it
2. Navigate back to the parent menu using the Option key (Mac) or Alt key (Windows)
3. Breadcrumb navigation is automatically displayed when you're in a nested menu
4. Categories are visually distinguished from regular options
Example of nested options structure:
```jsx
const options = [
{
label: "Category A",
children: [
{ label: "Option A1", value: "a1" },
{ label: "Option A2", value: "a2" },
{
label: "Subcategory",
children: [
{ label: "Nested Option 1", value: "n1" },
{ label: "Nested Option 2", value: "n2" },
],
},
],
},
{
label: "Category B",
children: [
{ label: "Option B1", value: "b1" },
{ label: "Option B2", value: "b2" },
],
},
];
```
| Prop | Type | Default | Description |
| ---------------------------- | ---------------------------------------------------------- | ------------ | -------------------------------------------------------------- |
| `inputRef` | `React.RefObject<HTMLInputElement \| HTMLTextAreaElement>` | Required | Reference to the input or textarea element |
| `options` | `ZeptoTypeAheadOption[]` | `[]` | Array of options to display in the dropdown |
| `triggerChar` | `string` | `'/'` | Character that triggers the typeahead dropdown |
| `position` | `'top' \| 'bottom' \| 'cursor'` | `'bottom'` | Position of the dropdown relative to the input |
| `activateMode` | `'single' \| 'multiple'` | `'multiple'` | Whether typeahead can be triggered multiple times in the input |
| `typeAheadContainerStyles` | `React.CSSProperties` | - | Custom styles for the typeahead container |
| `typeAheadOptionStyles` | `React.CSSProperties` | - | Custom styles for each option |
| `typeAheadActiveOptionStyle` | `React.CSSProperties` | - | Custom styles for the active option |
| `typeAheadOptionValueStyles` | `React.CSSProperties` | - | Custom styles for option values |
| `onSelect` | `(option: ZeptoTypeAheadOption) => void` | - | Callback fired when an option is selected |
| `searchCallback` | `(label: string, value: string) => boolean` | - | Custom search function for filtering options |
| `renderOption` | `({ option, isActive, onClick }) => React.ReactNode` | - | Custom render function for options |
| `renderHeader` | `(props: ZeptoTypeAheadHeaderProps) => React.ReactNode` | - | Custom render function for the typeahead header |
| `onWidgetStateChange` | `(state: ZeptoTypeAheadWidgetState) => void` | - | Callback fired when widget state changes |
```typescript
type ZeptoTypeAheadOption = {
label: string;
description?: string;
value?: string;
children?: ZeptoTypeAheadOption[];
};
type ZeptoTypeAheadPosition = "top" | "bottom" | "cursor";
type ZeptoTypeAheadActivateMode = "single" | "multiple";
type ZeptoTypeAheadHeaderProps = {
onClose: () => void;
onBack: () => void;
currentCategory?: ZeptoTypeAheadOption;
nestedPath: ZeptoTypeAheadOption[];
};
type ZeptoTypeAheadWidgetState = {
isActive: boolean;
currentSearch: string;
selectedOption?: ZeptoTypeAheadOption;
};
```
```jsx
<Typeahead
inputRef={inputRef}
options={options}
typeAheadContainerStyles={{
border: "1px solid #ccc",
borderRadius: "4px",
}}
typeAheadOptionStyles={{
padding: "12px",
fontSize: "14px",
}}
typeAheadActiveOptionStyle={{
background: "#f0f0f0",
}}
/>
```
```jsx
<Typeahead
inputRef={inputRef}
options={options}
renderOption={({ option, isActive, onClick }) => (
<div
onClick={onClick}
style={{
background: isActive ? "#e0f7fa" : "white",
display: "flex",
alignItems: "center",
padding: "8px 16px",
}}
>
<div style={{ marginRight: "8px" }}>🔍</div>
<div>
<div style={{ fontWeight: 500 }}>{option.label}</div>
{option.description && (
<div style={{ fontSize: "0.8em", color: "#666" }}>
{option.description}
</div>
)}
</div>
</div>
)}
/>
```
```jsx
import React, { useState, useEffect, useRef } from "react";
import { Typeahead } from "@zeptonow/input-typeahead";
function AsyncTypeahead() {
const inputRef = useRef(null);
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [currentInput, setCurrentInput] = useState("");
// Track input changes manually
useEffect(() => {
const input = inputRef.current;
if (!input) return;
const handleInput = () => {
setCurrentInput(input.value);
};
input.addEventListener("input", handleInput);
return () => input.removeEventListener("input", handleInput);
}, []);
// Fetch data when input changes
useEffect(() => {
if (!currentInput.includes("/")) return;
const query = currentInput.split("/").pop();
if (!query) return;
setLoading(true);
fetchData(query).then((results) => {
// Transform API results to match ZeptoTypeAheadOption format
const formattedResults = results.map((item) => ({
label: item.name,
value: item.id,
description: item.description,
}));
setOptions([
{
label: "Search Results",
children: formattedResults,
},
]);
setLoading(false);
});
}, [currentInput]);
return (
<div>
{loading && <div>Loading suggestions...</div>}
<input
ref={inputRef}
type="text"
placeholder="Type / followed by search term"
/>
<Typeahead
inputRef={inputRef}
options={options}
triggerChar="/"
position="bottom"
onSelect={(option) => console.log("Selected:", option)}
/>
</div>
);
}
// Mock API function
function fetchData(query) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(
[
{ id: "1", name: "Result 1", description: "First search result" },
{ id: "2", name: "Result 2", description: "Second search result" },
{ id: "3", name: "Result 3", description: "Third search result" },
].filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase()),
),
);
}, 500);
});
}
```
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.