SearchField

SearchField is a thin wrapper around Input that bakes in the search-field conventions: a left-aligned search glyph, a pill shape, an inline clear button that fades out when the query is empty, and an onClear that resets the value for you. Reach for it as a floating pill inside a Popover, List, or command palette — or wrap it in a sticky Surface to pin it as a header bar above a scrolling list.

<Surface outline className="w-72 rounded-3xl">
  <div className="max-h-72 overflow-auto">
    <Surface
      className="sticky top-0 z-20 rounded-t-cladd-popover"
      contentClassName="p-1.5"
      outline
    >
      <SearchField
        value={query}
        onChange={setQuery}
        placeholder="Search projects"
      />
    </Surface>
    <List>
      {filtered.length === 0 ? (
        <ListItem className="text-cladd-fg-softer">No matches</ListItem>
      ) : (
        filtered.map((p) => (
          <ListButton key={p} icon={<NoteIcon />}>
            {p}
          </ListButton>
        ))
      )}
    </List>
  </div>
</Surface>

Usage

import { List, ListButton, SearchField } from '@cladd-ui/react';
 
const [query, setQuery] = useState('');
 
<SearchField value={query} onChange={setQuery} placeholder="Search projects" />;

SearchField is controlled — pass value and an onChange handler. The handler also fires with '' when the user presses the inline clear button, so you don't need a separate onClear. Everything else is plain Input — every InputProps prop (size, color, rounded, clearButton, prefix, suffix, …) passes straight through, with the search-field defaults applied (size="lg", rounded, clearButton, placeholder="Search", and the search glyph as the leading icon).

For the pinned header-bar layout, wrap the field in a sticky Surface yourself:

<Surface outline className="w-72 rounded-3xl">
  <Surface
    className="sticky top-0 z-20 rounded-t-cladd-popover"
    contentClassName="p-1.5"
    outline
  >
    <SearchField value={query} onChange={setQuery} />
  </Surface>
  <List>{/* …filtered rows… */}</List>
</Surface>

Examples

Sizes

size accepts sm, md, lg (default), xl, 2xl — the same scale as Input, Button, and the rest of the form controls. Match the field's size to the rows of the List underneath so they read as one stack: a sm filter for a dense sidebar, an xl field for a hero command palette.

<Surface outline className="w-80 rounded-3xl">
  <Surface
    className="sticky top-0 z-20 rounded-t-cladd-popover"
    contentClassName="p-1.5"
    outline
  >
    <SearchField
      size="lg"
      value={query}
      onChange={setQuery}
      placeholder="Search projects"
    />
  </Surface>
  <List>
    {PROJECTS.filter((p) => matches(query, p))
      .slice(0, 3)
      .map((p) => (
        <ListButton key={p} icon={<NoteIcon />}>
          {p}
        </ListButton>
      ))}
  </List>
</Surface>

Inside a popover

Render SearchField directly inside a Popover or List to get a floating pill that reads as one row among many — useful below a SectionTitle, between ListSeparators, or as the first row of an action menu. Add horizontal margins (mx-2) so it doesn't bleed into the popover edge; that's the pattern Select uses for its built-in search.

<PopoverRoot defaultOpen>
  <PopoverTrigger>
    <Button>Run command</Button>
  </PopoverTrigger>
  <Popover className="w-64" offset={8} closeOnBackdropClick={false}>
    <SectionTitle className="px-4 pt-4">Commands</SectionTitle>
    <SearchField
      value={query}
      onChange={setQuery}
      placeholder="Filter commands"
      className="mx-2 mt-2 w-auto"
    />
    <List>
      {filtered.length === 0 ? (
        <ListItem className="text-cladd-fg-softer">No matches</ListItem>
      ) : (
        filtered.map((c) => (
          <ListButton
            key={c.id}
            after={
              <span className="font-mono text-cladd-fg-softer">
                {c.kbd}
              </span>
            }
          >
            {c.label}
          </ListButton>
        ))
      )}
    </List>
  </Popover>
</PopoverRoot>

Playground

size is the main knob. Toggle the layout switch to see the two composition patterns side-by-side: the floating pill on its own (drop it anywhere) versus a sticky Surface wrapper that pins it as a header above a scrolling list (rounded-t-cladd-popover to align with the rounded parent surface).

<Surface outline className="w-72 rounded-3xl">
  {inset && <SectionTitle className="px-4 pt-4">Search</SectionTitle>}
  {inset ? (
    <SearchField
      size="lg"
      value={query}
      onChange={setQuery}
      placeholder="Search projects"
      className={'mx-2 mt-2 w-auto'}
    />
  ) : (
    <Surface
      className="sticky top-0 z-20 rounded-t-cladd-popover"
      contentClassName="p-1.5"
      outline
    >
      <SearchField
        size="lg"
        value={query}
        onChange={setQuery}
        placeholder="Search projects"
      />
    </Surface>
  )}

  <List>
    {filtered.slice(0, 4).map((p) => (
      <ListButton key={p} icon={<EnvelopeIcon />}>
        {p}
      </ListButton>
    ))}
    {filtered.length === 0 && (
      <ListItem className="text-cladd-fg-softer">No matches</ListItem>
    )}
  </List>
</Surface>

API Reference

Inherits from InputProps
onChange: (value: string, event?: ChangeEvent<HTMLInputElement>) => voidFires on every keystroke. Also fires with '' when the clear button is pressed.