Search Components
A compound autocomplete surface — an input, a results dropdown, and an optional suggestion rail. The three parts discover each other through context so the consumer can lay them out freely. Pass items, plug a custom filter and onSelect, and the widget covers any "list + query + pick" use case.
Preview
Try
Customize
Behaviour
8
Display
Installation
npx shadcn@latest add https://craftbits.dev/r/search-components.jsonUsage
import { SearchComponents, type SearchComponentsItem } from "@craft-bits/core";
const items: SearchComponentsItem[] = [
{ id: "trie", label: "Prefix Trie", description: "Trie cache" },
{ id: "debounce", label: "Debounce", description: "Input rate limit" },
];
export function Demo() {
return (
<SearchComponents.Root items={items} onSelect={(item) => console.log(item)}>
<SearchComponents.Box placeholder="Search…" />
<SearchComponents.Results />
<SearchComponents.Suggestions title="Try" />
</SearchComponents.Root>
);
}Swap the filter for prefix-only, fuzzy, or whatever model the consumer wants — the signature is (item, query) => boolean.
Anatomy
- Root. Holds the query and open state, computes the filtered results, and exposes them via context. Accepts controlled
query/onQueryChangeandopen/onOpenChangeprops for full external control. - Box. The combobox-compliant input. Owns ArrowDown / ArrowUp / Enter / Escape, mirrors
aria-activedescendant, and renders a leading magnifier glyph plus a trailing "Esc to clear" pill. - Results. A
role="listbox"dropdown that animates in when the query is non-empty. Each row isrole="option"witharia-selectedmirroring the highlight. Substring matches are wrapped in a<mark>by default. - Suggestions. A chip rail that only shows when the query is empty. Picking a chip fills the input with that label.
Understanding the component
- Compound context. Root builds a single context value and renders its children inside a wrapping div. Each child part reads that value via
useContext— the parts can sit in any order, but Box and Results commonly stack so Results can absolute-position underneath. - Filter. The default predicate is a case-insensitive substring match on
label. Override per-Root viafilter. - Keyboard. ArrowDown / ArrowUp move the highlight and open the dropdown if it was closed. Enter selects the highlighted row when the query is non-empty. Escape clears the query on first press, then blurs on the second.
- Highlighted match. Results wraps the first case-insensitive occurrence of the query in each label with a
<mark>. Disable viahideHighlight, or passrenderItemfor full control. - Motion. The Results panel grows in with
SPRINGS.snapand fades out. Animations short-circuit underprefers-reduced-motion.
Props
SearchComponents.Root
| Prop | Type | Default | Description |
|---|---|---|---|
items | SearchComponentsItem[] | required | Source list filtered against the query. |
suggestions | SearchComponentsItem[] | first 5 of items | Chips shown when the query is empty. |
filter | (item, query) => boolean | substring | Match predicate. |
query / defaultQuery | string | '' | Controlled / uncontrolled query value. |
onQueryChange | (next) => void | — | Fires on every query mutation. |
onSelect | (item) => void | — | Fires when the user picks a row. |
maxResults | number | 8 | Hard cap on the result list. |
open / onOpenChange | boolean | uncontrolled | Override the open state. |
SearchComponents.Box
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | 'Search…' | Placeholder text. |
ariaLabel | string | 'Search' | Accessible name for the input. |
hideIcon | boolean | false | Hide the leading magnifier glyph. |
hideClear | boolean | false | Hide the trailing Esc pill. |
SearchComponents.Results
| Prop | Type | Default | Description |
|---|---|---|---|
hideHighlight | boolean | false | Skip the highlight wrap on labels. |
renderItem | (item, info) => ReactNode | — | Custom row renderer. |
emptyLabel | ReactNode | 'No results' | Empty-state message. |
SearchComponents.Suggestions
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | 'Try' | Caption above the chip rail. |
onPick | (item) => void | — | Fires when a suggestion chip is pressed. |
SearchComponentsItem
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
label | string | Visible label and default match target. |
description | ReactNode | Optional sub-label rendered in a muted tone. |
value | unknown | Free-form payload surfaced back via onSelect. |
Accessibility
- The input carries
role="combobox",aria-expanded,aria-controls,aria-autocomplete="list", andaria-activedescendantso screen readers announce the highlighted option as the user arrows through results. - The dropdown is
role="listbox"witharia-label="Search results". Each row isrole="option"witharia-selectedmirroring the highlight. - The Esc clear pill has an explicit
aria-label="Clear search". - All animations short-circuit under
prefers-reduced-motion— the dropdown still toggles, just without the slide. data-cb-edu="search-components"anddata-cb-open="true | false"ride the wrapper for tone-specific styling without monkey-patching.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-autocomplete/ui/SearchComponents.tsx). The original bundled four pieces —PersistentSearch,NetworkTimeline,TrieVisualizer, and ahighlightMatchhelper — all bound to a project-leveluseAutocompletecontext that wired a prefix-trie engine, an in-flight request store, and a feature-flag set ('debounce', 'abortController', 'trieCache', 'keyboardNav', 'matchHighlight', 'generationCounter', 'networkError', 'accessibility'). This rewrite drops the engine coupling, the network timeline, the trie visualiser, and the feature-flag badges, and generalises the surface to a context-driven compound —SearchComponents.Root/.Box/.Results/.Suggestions— with items + filter + onSelect as the only required wiring.