Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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

    CommandSearch is 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

    SearchResults is 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:

src/pages/ResultsPage.tsx

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.

Switch search

Results for one of two searches, each section linking to every result.

Results for "shipping"

2 results

Variants and states

Its title says what was searched for, then a section for each source with every result.

Props

SearchResults

PropTypeDefaultDescription
query

required

SearchQueryWhat was searched for, as CommandSearch hands it to resultsHref or onSearch.
sources

required

readonly SearchSource[]Where to look: one section each, in this order, with every result rather than the panel's five.
emptyHintReactNodeA line under "No results", such as a link to the help site.
pageSizenumber50How many results a section shows before a "Show more" button under it adds as many again. Defaults to 50.
labelsPartial<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.
asideReactNodeA 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.
asideLabelstring"Details"Names the right rail's landmark, such as "Customer details". Defaults to "Details".
footerReactNodeA 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.

FieldTypeDefaultDescription
text

required

stringThe words in the field, without the finished filters.
filters

required

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.

FieldTypeDefaultDescription
remotebooleanLeave it out, or false, for a source that answers at once. true: it answers later.
search

required

(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.
id

required

stringIdentifies it among the sources.
label

required

stringIts group's heading, such as "Orders".
limitnumber5How 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.

FieldTypeDefaultDescription
title

required

(search: string) => stringThe page's title, such as Results for "decaf".
count

required

(count: number) => stringHow many results there are, such as "12 results".
loading

required

stringWhat a section waiting on a server says.
noResults

required

(search: string) => stringSays nothing matched.
showMore

required

(count: number) => stringThe 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.

FieldTypeDefaultDescription
key

required

stringThe filter's key.
value

required

stringWhat it's set to.

SearchItem

One result in a `CommandSearch` group or on a `SearchResults` page.

FieldTypeDefaultDescription
id

required

stringIdentifies it within its source.
label

required

stringIts words, with the characters that match the search highlighted.
pathreadonly string[]Where it sits, shown before the label with a chevron after each part, such as ["Settings"].
detailstringA short line after the label in text-secondary, such as a handle or an order's total.
iconComponentType<IconComponentProps>An icon before it, such as CircleHelp for a help article.
mediaReactNodeA small element before it in place of an icon, such as a 20px Avatar.
hrefstringWhere 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.

KeysWhat they do
EnterOpens the focused result
Enter or SpaceOn 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.

See it in the roastery demo.

  • 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.