Search and results
The search in an app's top bar, and the page of every result it leads to.
Search in a buttery app has two parts: a field in the top bar, and the page of every result it leads to. Use both, so a search that's more than a quick jump has somewhere to go.
The two parts
The field
CommandSearchis the search in the top bar. As a person types, it suggests filters and shows grouped results, with a link to every result.The results page
SearchResultsis the page of every result, grouped by where each was found: a section for each source, with every result rather than the panel's five.
From the field to the page
Sending a search from the field opens the results page with the same query. parseSearch(value, filters, { sent: true }) reads the field's text and its finished filters, such as status:shipped, into one query both parts understand:
import type { ReactElement } from "react";
import { SearchResults, parseSearch } from "@butteryui/react";
import type { SearchFilter, SearchSource } from "@butteryui/react";
export function ResultsPage({
search,
filters,
sources,
}: {
readonly search: string;
readonly filters: readonly SearchFilter[];
readonly sources: readonly SearchSource[];
}): ReactElement {
return (
<SearchResults
query={parseSearch(search, filters, { sent: true })}
sources={sources}
/>
);
}SearchResults
When to use it
Use it for
- Where CommandSearch's View all results leads
- A search that needs more than five results a group, or a link people can share
Not for
- Filtering one table: use DataTable with a SearchField
- A few results as you type: use CommandSearch
Examples
Help results
Help articles from a pretend server, the section waiting until they arrive.
Results for "payout"
Searching
Help
Paged orders
Every shipped order, five at a time, a button under them showing the next five.
Results for "shipped"
12 results
Switch search
Results for one of two searches, each section linking to every result.
Results for "shipping"
2 results
Go to
1 result
Orders
1 result
Variants and states
Results for a search
Its title says what was searched for, then a section for each source with every result.
Results for "shipping"
4 results
Go to
2 results
Orders
2 results
Props
SearchResults
| Prop | Type | Default | Description |
|---|---|---|---|
queryrequired | SearchQuery | What was searched for, as CommandSearch hands it to resultsHref or onSearch. | |
sourcesrequired | readonly SearchSource[] | Where to look: one section each, in this order, with every result rather than the panel's five. | |
emptyHint | ReactNode | A line under "No results", such as a link to the help site. | |
pageSize | number | 50 | How many results a section shows before a "Show more" button under it adds as many again. Defaults to 50. |
labels | Partial<SearchResultsLabels> | Its words, for another language or your own. | |
width | "contained" | "medium" | "narrow" | "full" | "contained" | How wide the page gets, its padding included, centered in the column: "contained" (the default),
1280px, for index and table pages; "medium", 960px, for a settings detail page; "narrow", 820px, for
a dense form or a preferences matrix; or "full", the column's width. |
aside | ReactNode | A right rail beside the page, 320px wide from 1040px of the shell's column beside a full sidebar (1280 of shell, with no side panel docked), under the page otherwise, including beside a narrow shell's icon rail. | |
asideLabel | string | "Details" | Names the right rail's landmark, such as "Customer details". Defaults to "Details". |
footer | ReactNode | A small footer that ends the page under a hairline, such as a feedback link and the keyboard
shortcuts, in body-sm and text-secondary. |
It also takes Omit<ComponentPropsWithRef<"main">, "children">.
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. |
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. |
SearchResultsLabels
The words a `SearchResults` page shows.
| Field | Type | Default | Description |
|---|---|---|---|
titlerequired | (search: string) => string | The page's title, such as Results for "decaf". | |
countrequired | (count: number) => string | How many results there are, such as "12 results". | |
loadingrequired | string | What a section waiting on a server says. | |
noResultsrequired | (search: string) => string | Says nothing matched. | |
showMorerequired | (count: number) => string | The button under a section with more to show, such as "Show 50 more". |
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. |
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. |
Accessibility
A main landmark with the search as its h1 and the count under it. Each source is a region named by its heading, its results links in a list, read with their path first; a long section shows 50 at a time, and its Show more button hands focus to the first row it shows. A section waiting on a server shows a loading pill that says Searching. With nothing found, the empty state's heading says so.
| Keys | What they do |
|---|---|
| Enter | Opens the focused result |
| Enter or Space | On Show more, shows the next results and moves focus to the first of them |
Motion
It doesn't move.
With reduced motion: Nothing changes.
Layout
An AppPage, at most 1280px wide unless its width says otherwise, with the search as its title and the count under it. Each result is a row at least 40px tall across the page, the whole row its hit area, its path and label and its detail each truncating and shown in full on hover; a section waiting on a server keeps a 64px-tall place for its loading pill.
Related
See it in the roastery demo.
Related
- CommandSearch
The top bar's search: grouped results from typed sources, filters and recent searches.
- App shell
The frame of a whole app, as an editor or a dashboard, and the page that goes in it.