Skip to content
Tier

Search input

Pro

A search field. onValueChange hears every keystroke; onSearch hears the text once typing pauses (debounceMs), at once on Enter, and with "" at once when the field is cleared, by its Clear button or Escape. Focus stays in the field when Clear is pressed, since the button goes as the text does.

label names the field (a placeholder isn't a name). shortcut focuses it from anywhere on the page and is shown in the field and in aria-keyshortcuts. While busy a pulsing dot says it is searching. results is announced politely once a search settles: the first render after onSearch in which busy is false, so set busy (or the new results) in onSearch itself. null says nothing, for a count the app doesn't know.

For a button that opens a search surface use SearchTrigger; for the command palette, CommandTrigger.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/search-input

Brings in: kbd, locale, motion, tokens, use-announcer, use-shortcut, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { SearchInput, SearchTrigger } from "@/components/candor/ui/search-input"

const runs = ["Deploy api to staging", "Deploy web to production", "Backfill orders", "Rotate signing keys", "Rebuild search index"]

export default function Example() {
  const [query, setQuery] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [results, setResults] = React.useState<number | null>(null)
  const [opened, setOpened] = React.useState(0)
  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
  const shown = runs.filter((r) => r.toLowerCase().includes(query.toLowerCase()))

  return (
    <div className="flex max-w-sm flex-col gap-3 rounded-xl border bg-card p-4">
      <SearchInput
        label="Search runs"
        placeholder="Name or id"
        shortcut="/"
        busy={busy}
        results={results}
        onSearch={(q) => {
          clearTimeout(timer.current)
          if (!q) {
            setQuery("")
            setResults(null)
            return
          }
          // A stand-in for a server search that takes a moment.
          setBusy(true)
          timer.current = setTimeout(() => {
            setQuery(q)
            setResults(runs.filter((r) => r.toLowerCase().includes(q.toLowerCase())).length)
            setBusy(false)
          }, 400)
        }}
      />
      <ul className="flex flex-col gap-1 text-sm">
        {shown.map((r) => (
          <li key={r}>{r}</li>
        ))}
      </ul>
      <SearchTrigger onOpen={() => setOpened((n) => n + 1)} shortcut="mod+k" />
      <p className="text-xs text-muted-foreground">Search opened {opened} times.</p>
    </div>
  )
}

<SearchInput>

PropTypeDefaultDescription
labelrequiredstring—The field's accessible name ("Search runs"). Required: the placeholder isn't a name.
valuestring | undefined—The text, when the app holds it; pair it with `onValueChange`. Setting it to text other than what the person typed drops the search their typing was waiting on.
defaultValuestring | undefined""The text it starts with, when the field holds it.
onValueChange((value: string) => void) | undefined—Called with the text on every keystroke, and with "" when it is cleared.
onSearch((value: string) => void) | undefined—Called with the text to search for: after `debounceMs` without typing, at once on Enter, and with "" at once when cleared.
debounceMsnumber | undefined250How long typing must pause before `onSearch`, in milliseconds. 250 by default.
shortcutstring | undefined—A `useShortcut` combo ("mod+k", "/") that focuses the field from anywhere on the page, shown in it while it is empty.
busyboolean | undefinedfalseA search is running: a pulsing dot shows, and the field is `aria-busy`.
resultsnumber | null | undefined—How many results the last search found, announced once it settles ("12 results", "No results"); `null` says nothing.
placeholderstring | undefined—Hint text inside the empty field ("Name, id or owner"). Not a name: `label` is.
classNamestring | undefined—Classes for the wrapper, merged with Candor's.

<SearchTrigger>

A button drawn like a search field that opens a search surface (a dialog, a page, a panel), for a top bar or a sidebar. Its name is "Search" (or label), and shortcut, pressed anywhere on the page, opens it too: the keys are drawn on it and given in aria-keyshortcuts. For the command palette, CommandTrigger does the same.

PropTypeDefaultDescription
onOpenrequired() => void—Opens the search surface; called on a press and on the shortcut.
labelstring | undefined—The button's words. "Search" by default.
shortcutstring | undefined—A `useShortcut` combo ("mod+k", "/") that opens it from anywhere on the page, drawn on the button.
classNamestring | undefined—Classes for the button, merged with Candor's.

Strings

The fixed words, in English by default. Change any of them under searchInput in <LocaleProvider strings={{ … }}>; the table is exported as searchInputStrings.

KeyEnglish
clearClear search
searchingSearching
results{ one: "{count} result", other: "{count} results" }
noResultsNo results
triggerSearch

Motion

MomentFamilyWhat it does
busyliveWhile `busy` is set, a dot in the field pulses to say a search is running; it stops as soon as `busy` is cleared, and under reduced motion it is still.

Set with --cd-motion-search-input-<moment>-* in your brand file; the families are in the design reference.