FairGarden Design

Search

Where typing beats browsing: site and collection search on Base UI's Autocomplete, in a search landmark. Don't make search the only route into content that also needs browsing, and pick a Combobox for one value from a long known set.

import { Search } from '@fairgarden-private/design/components/Search'

label names both the field and the landmark; the placeholder ("Search the guide…") never replaces it. The field is the Input's box, with the ring on any focus: in the butted build it wraps the field and the submit together. Matches are marked by weight, not color, and an empty result is a plain-word status with a next step, never a red error. Show one docked bar at a time.

Kinds and states

Kinds and states

boxed (default) is the field with the butted submit: the icon-only 64 × 48 px cell at base, labelled from --md-n-above with submitLabelled. ruled sets the field between two --border-size-2 rules. trigger is the header's icon-only Button; wire it to open a Dialog sheet below --lg-n-above and a docked bar from it. docked puts the boxed field in the full-width top bar. Suggestions come in groups under a rule, with an optional type icon and an italic secondary name; status announces "12 results", and seeAllHref adds the standalone "See all results" link.

boxed

Type "egret", then press Enter.

ruled

trigger

docked

Scroll this frame: the docked bar holds the top edge, alone.

Herons stand still in the shallows and strike fast.

Egrets hunt the same water in brighter white.

Bitterns hide in the reeds and point their bills skyward.

SearchKinds.tsx
'use client'

import * as React from 'react'
import { Search, type SearchSuggestionGroup } from '@fairgarden-private/design/components/Search'
import styles from './states.module.css'

const suggestions: SearchSuggestionGroup[] = [
  {
    label: 'Species',
    items: [
      { value: 'Great Egret', secondaryName: 'Ardea alba', href: '#egret' },
      { value: 'Snowy Egret', secondaryName: 'Egretta thula', href: '#snowy' },
      { value: 'Reddish Egret', secondaryName: 'Egretta rufescens', href: '#reddish' },
    ],
  },
  {
    label: 'Places',
    items: [
      { value: 'Egret Point', icon: 'chevron_right', href: '#point' },
      { value: 'Egret Marsh Trail', icon: 'chevron_right', href: '#marsh' },
    ],
  },
]

/**
 * Boxed with the butted submit and grouped suggestions, rule-bounded, the
 * header trigger, and the docked bar (inside a short scrolling frame).
 */
export function SearchKinds() {
  const [submitted, setSubmitted] = React.useState('')

  return (
    <div className={styles.stack}>
      <div>
        <p className={styles.label}>boxed</p>
        <Search
          label="Search the guide"
          placeholder="Search the guide…"
          items={suggestions}
          status="5 suggestions"
          seeAllHref={(query) => `#results-${encodeURIComponent(query)}`}
          submitLabelled
          onSubmit={(query) => setSubmitted(query)}
        />
        <p className={styles.label} aria-live="polite">
          {submitted ? `Searched for "${submitted}"` : 'Type "egret", then press Enter.'}
        </p>
      </div>
      <div>
        <p className={styles.label}>ruled</p>
        <Search kind="ruled" label="Filter events" placeholder="Filter events…" />
      </div>
      <div>
        <p className={styles.label}>trigger</p>
        <Search kind="trigger" label="Search" onClick={() => setSubmitted('the trigger')} />
      </div>
      <div>
        <p className={styles.label}>docked</p>
        <div className={styles.dockFrame}>
          <Search kind="docked" label="Search the guide" items={suggestions} />
          <p>Scroll this frame: the docked bar holds the top edge, alone.</p>
          <p>Herons stand still in the shallows and strike fast.</p>
          <p>Egrets hunt the same water in brighter white.</p>
          <p>Bitterns hide in the reeds and point their bills skyward.</p>
        </div>
      </div>
    </div>
  )
}

Primary and secondary

Primary and secondary

Search's own parts take primary; the submit takes the scope's action scale, and the popup the white scope's defaults.

SearchColor.tsx
'use client'

import { Search } from '@fairgarden-private/design/components/Search'
import styles from './color.module.css'

const items = [{ value: 'Owls' }, { value: 'Otters' }, { value: 'Orchids' }]

/**
 * `primary` recolors the field, magnifier and rows' ink; the submit keeps
 * the scope's action scale, as every `solid` Button does.
 */
export function SearchColor() {
  return (
    <div className={styles.stack}>
      <Search label="Search, primary plum" primary="plum" items={items} />
      <Search kind="ruled" label="Filter, primary indigo" primary="indigo" items={items} />
    </div>
  )
}

On grounds

On paper and forest

The field keeps its open box on every ground; the rules are the ground's --primary12.

paper

forest

SearchGrounds.tsx
'use client'

import { PresetGround } from '@/components/PresetGround'
import { Search } from '@fairgarden-private/design/components/Search'
import styles from './grounds.module.css'

const items = [{ value: 'Moths' }, { value: 'Mosses' }, { value: 'Mushrooms' }]

const presets = ['paper', 'forest'] as const

/**
 * On forest the submit stays amber with its edge equal to its fill, and
 * the suggestions popup is the white scope, which follows the page mode.
 */
export function SearchGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <p className={styles.name}>{preset}</p>
          <Search label={`Search on ${preset}`} items={items} />
          <Search kind="ruled" label={`Filter on ${preset}`} items={items} />
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

Search in a search landmark. Matches are marked by weight, never color; an empty result is a plain-word status, never a red error (P10). Show one docked bar at a time.

PropTypeDescription
name
string | undefined

The query’s name in a native submission. Default q.

label
string

The field’s label and the landmark’s name (“Search the guide”). A placeholder never replaces it.

defaultValue
string | undefined

The initial query.

value
string | undefined

The query, controlled.

onValueChange
((value: string) => void) | undefined
onClick
| ((
    event: React.MouseEvent<
      HTMLButtonElement,
      MouseEvent
    >,
  ) => void)
| undefined

Opens the app’s search sheet (a Dialog below --lg-n-above) or docked bar.

action
string | undefined

A native form action, used when onSubmit is not given.

emptyText
((query: string) => React.ReactNode) | undefined

The empty status. Default “No results for ‘[query]'"; add a next step where you can.

filter
__type | null | undefined

Base UI’s filter; null when the server already filtered the items.

hideSubmit
boolean | undefined

Hides the butted submit (boxed and docked); Enter still submits.

items
SearchItems | undefined

The suggestions, or titled groups. Pass filter={null} when the server filters them.

kind
'boxed' | 'ruled' | 'docked' | 'trigger' | undefined

boxed (default): the field box with the butted submit. ruled: the field between two --border-size-2 rules. docked: the boxed field in the full-width top bar. trigger: the header’s icon-only Button that opens the search sheet or bar.

labelVisible
boolean | undefined

Shows label above the field. Default false: it names the input for assistive technology.

loading
boolean | undefined

Shows “Searching…” in the status row while suggestions load.

onSubmit
| ((
    query: string,
    event: React.FormEvent<HTMLFormElement>,
  ) => void)
| undefined

Called with the query on submit (Enter, or the submit cell); the page does not reload.

placeholder
string | undefined

Default “Search…", ending in the ellipsis character.

primary
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: field, rules, rows and focus ring. Never defaulted [D133].

secondary
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: accepted; the submit takes the scope’s action scale.

seeAllHref
((query: string) => string) | undefined

The “See all results” row’s destination for a query.

seeAllText
((query: string) => React.ReactNode) | undefined

The “See all” row’s words. Default “See all results for ‘[query]'".

status
React.ReactNode | undefined

Status text: “12 results”. Announced politely.

submitLabel
string | undefined

The submit cell’s accessible name (and label from --md-n-above when submitLabelled). Default “Search”.

submitLabelled
boolean | undefined

Shows the submit’s label from --md-n-above. Default false: the icon-only cell.

submitOnItemClick
boolean | undefined

Submits the chosen suggestion when a row is pressed.

disabled
boolean | undefined
className
string | undefined

Class names for the landmark, added after the module’s own.

render
ReactElement | undefined

Base UI render, to compose the trigger with a Dialog trigger.

search
SearchProps

Props for Search. kind="trigger" takes only the trigger props; the other kinds take the field props.

type SearchProps = (
  | {
      kind?: 'boxed' | 'ruled' | 'docked';
      labelVisible?: boolean;
      placeholder?: string;
      value?: string;
      defaultValue?: string;
      onValueChange?: (value: string) => void;
      onSubmit?: (query: string, event: React.FormEvent<HTMLFormElement>) => void;
      action?: string;
      name?: string;
      items?: SearchItems;
      filter?: __type | null;
      status?: React.ReactNode;
      loading?: boolean;
      emptyText?: (query: string) => React.ReactNode;
      seeAllHref?: (query: string) => string;
      seeAllText?: (query: string) => React.ReactNode;
      submitLabel?: string;
      submitLabelled?: boolean;
      hideSubmit?: boolean;
      submitOnItemClick?: boolean;
      disabled?: boolean;
    }
  | {
      kind: 'trigger';
      onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
      render?: ReactElement;
    }
) & {
  /**
   * The field's label and the landmark's name ("Search the guide"). A
   * placeholder never replaces it.
   */
  label: string;
  /** Class names for the landmark, added after the module's own. */
  className?: string;
  /** Primary Radix scale: field, rules, rows and focus ring. Never defaulted [D133]. */
  primary?:
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'ruby'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /** Secondary Radix scale: accepted; the submit takes the scope's action scale. */
  secondary?:
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'ruby'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
}
SearchSuggestion

One suggestion row.

type SearchSuggestion = {
  /** The text the suggestion fills in (and submits). */
  value: string;
  /** The row's words. Default: `value`. */
  label?: string;
  /** An italic secondary name in --role-muted (a Latin name, a place). */
  secondaryName?: string;
  /** An optional type icon (inline tier). */
  icon?:
    | 'search'
    | 'arrow_forward'
    | 'arrow_upward'
    | 'expand_more'
    | 'close'
    | 'remove'
    | 'add'
    | 'check'
    | 'circle'
    | 'chevron_right'
    | 'chevron_left'
    | 'menu'
    | 'more_horiz'
    | 'play_arrow'
    | 'pause'
    | 'download'
    | 'zoom_in'
    | 'zoom_out'
    | 'recenter'
    | 'help'
    | 'mail';
  /** Makes the row a link to this page. */
  href?: string;
  disabled?: boolean;
}
SearchSuggestionGroup

A titled group of suggestions; each group after the first sits under a rule.

type SearchSuggestionGroup = {
  /** The group head, in `type-label` caps. */
  label: string;
  items: SearchSuggestion[];
}

Specification: DESIGN-SYSTEM.md §9.10 (search), with the field box of §10.1 and the butted cell of §10.3.