CommandSearch
The top bar's search: grouped results from typed sources, filters and recent searches.
import { CommandSearch } from "@butteryui/react";Demo
When to use it
Use it for
- The app-wide search in a TopBar: pages, records and help in one place
- A search that takes key:value filters, such as status:shipped
Not for
- Narrowing one table or list: use SearchField
- Choosing one value in a form: use Select
Examples
Order filters
Orders narrowed by a status filter typed as key:value, which turns into a chip.
Every order
Roastery search
A search across a roastery's pages and customers that remembers recent searches.
Nothing opened yet
Top bar search
The app's search in its top bar: finding an order opens it on the page under the bar.
Orders
Search for an order to open it here.
Variants and states
Pages, customers and help
Focus it with / or ⌘K. Pages and customers answer as you type; help asks a pretend server and shows a spinner row in its own group.
Props
CommandSearch
| Prop | Type | Default | Description |
|---|---|---|---|
style | MotionStyle | Inline styles for the field, which can include motion values. | |
label | string | "Search" | Names the field for screen readers, and shows as its placeholder unless there's one. Defaults to "Search". |
value | string | What's in the field, finished filters included, such as "status:shipped decaf". Pair with onValueChange. | |
defaultValue | string | What's in the field at first when uncontrolled. | |
onValueChange | ((value: string) => void) | Called with what's in the field on every change. | |
sourcesrequired | readonly SearchSource[] | Where it looks: one group each in the panel, in this order. | |
filters | readonly SearchFilter[] | The key:value filters people can type, suggested while the field is empty and as they type a key. | |
filtersHref | string | Where the "View more filters" link under an empty search goes. Leave it out for no link. | |
resultsHref | ((query: SearchQuery) => string) | Where "View all results" and Enter with no row chosen go, such as a SearchResults page. | |
onSearch | ((query: SearchQuery) => void) | Called when someone asks for every result: Enter with no row chosen, or "View all results". | |
storageKey | string | Remembers the last five searches under this key, listed while the field is empty. | |
shortcut | boolean | true | Whether / and ⌘K (Ctrl K) focus it from anywhere. Defaults to true. |
emptyHint | ReactNode | A line under "No results", such as a link to the help site. | |
suggestions | readonly SearchItem[] | Results listed while the field is empty, after the recent searches, such as the pages most people want. | |
didYouMean | ((query: SearchQuery) => string | undefined) | A better search for a query that found nothing, such as the closest word in the index, which the panel
offers as a row under "No results" that runs it. Return undefined for none. | |
preview | ((item: SearchItem, query: SearchQuery) => ReactNode) | Draws the highlighted result, or the first one, in a pane beside the rows whenever the window has room for it to the field's end, such as its description and a small demo. The pane is for the eye: it's inert, so focus stays in the field and screen readers hear the highlighted row. | |
labels | Partial<CommandSearchLabels> | Its words, for another language or your own. |
It also takes Omit<ComponentPropsWithRef<"input">, "size" | "value" | "defaultValue" | "children" | "type" | "style">.
SearchHighlight
| Prop | Type | Default | Description |
|---|---|---|---|
textrequired | string | The words to draw, such as a result's label. | |
queryrequired | string | The search, whose words are marked where they appear in text. |
SearchSource
Where a `CommandSearch` looks: one group in its panel, and one section on a `SearchResults` page.
| Field | Type | Default | Description |
|---|---|---|---|
remote | boolean | Leave it out, or false, for a source that answers at once. true: it answers later. | |
searchrequired | (query: SearchQuery) => readonly SearchItem[] | (query: SearchQuery, signal: AbortSignal) => Promise<readonly SearchItem[]> | With remote={false}: Finds what matches the query. With remote={true}: Finds what matches the query; signal aborts once the query has moved on. | |
idrequired | string | Identifies it among the sources. | |
labelrequired | string | Its group's heading, such as "Orders". | |
limit | number | 5 | How many results its group shows in the panel, at most. Defaults to 5. |
renderItem | ((item: SearchItem, query: SearchQuery) => ReactNode) | Draws one of its results in place of the standard row. |
SearchFilter
A filter people can type into a `CommandSearch` as `key:value`.
| Field | Type | Default | Description |
|---|---|---|---|
keyrequired | string | What they type before the colon, such as "status". | |
descriptionrequired | string | What it narrows by, shown at the end of its suggestion, such as "where an order has got to". | |
examplerequired | string | A value shown after the key while nothing is typed, such as "shipped". | |
values | readonly string[] | The values it takes, suggested as people type after the colon. Leave it out for free values. |
SearchQuery
What someone is searching for: their words and the filters they've finished.
| Field | Type | Default | Description |
|---|---|---|---|
textrequired | string | The words in the field, without the finished filters. | |
filtersrequired | readonly SearchToken[] | The finished filters, in the order they were typed. |
SearchItem
One result in a `CommandSearch` group or on a `SearchResults` page.
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | string | Identifies it within its source. | |
labelrequired | string | Its words, with the characters that match the search highlighted. | |
path | readonly string[] | Where it sits, shown before the label with a chevron after each part, such as ["Settings"]. | |
detail | string | A short line after the label in text-secondary, such as a handle or an order's total. | |
icon | ComponentType<IconComponentProps> | An icon before it, such as CircleHelp for a help article. | |
media | ReactNode | A small element before it in place of an icon, such as a 20px Avatar. | |
href | string | Where choosing it goes. | |
onSelect | (() => void) | What choosing it does, as well as going to href when there's one. |
CommandSearchLabels
The words a `CommandSearch` shows and announces.
| Field | Type | Default | Description |
|---|---|---|---|
resultsrequired | string | Names the list of rows under the field. | |
filtersrequired | string | Heads the filters it suggests. | |
recentrequired | string | Heads the recent searches. | |
moreFiltersrequired | string | The link to every filter, under an empty search. | |
allResultsrequired | string | The link to every result, under the groups. | |
noResultsrequired | (text: string) => string | Says nothing matched, such as No results for "decaf". | |
searchingrequired | (source: string) => string | The row a remote group shows while it's asking, such as "Searching orders". | |
clearrequired | string | Names the clear button. | |
removeFilterrequired | (token: string) => string | Names a chip's remove button, such as "Remove status:shipped". | |
filteringrequired | (tokens: readonly string[]) => string | Describes the field while it holds finished filters, such as "Filtered by status:shipped". | |
resultCountrequired | (count: number) => string | Announces how many results its sources found, such as "12 results". | |
suggestionCountrequired | (count: number) => string | Announces how many filters it suggests while nothing has been found, such as "4 suggestions". | |
suggestionsrequired | string | Heads the results suggested while the field is empty. | |
didYouMeanrequired | string | Heads the better search offered when nothing is found. |
SearchToken
A finished `key:value` filter, shown as a chip in the field.
| Field | Type | Default | Description |
|---|---|---|---|
keyrequired | string | The filter's key. | |
valuerequired | string | What it's set to. |
Also exported
| Export | Description |
|---|---|
formatSearch(query: SearchQuery): string | Writes a query back as the text a CommandSearch holds: each finished filter followed by a space, then the
words. parseSearch reads it back as the same query. |
matchRanges(label: string, text: string): readonly (readonly [number, number])[] | Where the words of text appear in label, ignoring case, as [start, end) ranges in order, merged where
they touch or overlap: what CommandSearch highlights. |
matchesSearch(text: string, ...fields: readonly (string | undefined)[]): boolean | Whether every word of text appears somewhere in fields, ignoring case: how the built-in sources match. |
pageSource({ pages, id = "pages", label = "Go to", limit, }: PageSourceOptions): LocalSearchSource | A source for the app's pages, matched by their names and the sections they sit in. Its results show their path before their name. It finds nothing while the search has filters, which pages don't take. |
parseSearch(value: string, filters: readonly SearchFilter[], { sent = false }: ParseSearchOptions = {}): SearchQuery | Reads what's in a CommandSearch field into its finished filters and its words. A key:value whose key is
one of filters is finished once a space follows it; until then it's still being typed. A sent search
({ sent: true }), such as the q of a results page, finishes the last one too. |
Accessibility
A combobox named by its label that controls a listbox named Results, in a panel with no role of its own. Focus stays in the field while the arrow keys move through the rows across groups and on to the footer's link (aria-activedescendant); Enter opens the row, or every result when no row is chosen; Escape clears, then closes. Each group is named by its heading. The field is described by its finished filters, and a polite status line says how many results its sources found, or that nothing matched, once typing pauses. / and ⌘K focus it. A chip's remove button is named for its filter and takes Tab, and Backspace at the start of the field removes the last chip. Keys an input method is composing with are left alone.
| Keys | What they do |
|---|---|
| / or ⌘K (Ctrl K) | Focuses the field from anywhere on the page, unless shortcut is false |
| ArrowDown or ArrowUp | Moves through the rows, across groups and on to the link to every result |
| Enter | Opens the highlighted row, or every result when no row is highlighted |
| Tab | Completes the filter key being typed, or takes the highlighted filter |
| Backspace | Removes the last filter chip, from the start of the field |
| Escape | Clears the field, then closes the panel, keeping focus in the field |
Motion
It changes shape as one surface, on `spring.smooth`, or `spring.morph` for a big change, rather than closing and reopening.
With reduced motion: The field takes its wider size at once as it gains focus, and back as it loses it, instead of growing.
Layout
The field is 34px tall (--bt-h-control) and 360px wide at rest (--bt-w-search), 600 while it has focus (--bt-w-search-open), never wider than its container; filter chips truncate before the text drops under 64px. The panel is as wide as the field, 8px under it, 160 to 480px tall within the window, with rows at least 32px tall whose text truncates.
Related
See it in the roastery demo.